Загрузка данных


// ==UserScript==
// @name         Pocket Option — симуляция для видео
// @namespace    local.pocket-demo-video
// @version      1.3.0
// @description  Отдельные постановочные сделки с таймером и явной маркировкой симуляции.
// @match        https://pocketoption.com/*/cabinet/demo-quick-high-low/*
// @match        https://pocketoption.com/*/cabinet/demo-quick-high-low/
// @grant        none
// @noframes
// @run-at       document-idle
// ==/UserScript==

(() => {
    'use strict';

    const HOST_ID = 'po-video-simulation';
    const allowed = () => location.hostname === 'pocketoption.com' &&
        /^\/[a-z]{2}(?:-[a-z]{2})?\/cabinet\/demo-quick-high-low\/?$/i.test(location.pathname) &&
        (!window.AppData || (window.AppData['is-demo'] !== 0 && window.AppData['is-demo'] !== '0' &&
            (!window.AppData.trading_account_type || window.AppData.trading_account_type === 'demo')));
    if (!allowed() || document.getElementById(HOST_ID)) return;

    const host = document.createElement('div');
    host.id = HOST_ID;
    host.style.cssText = 'all:initial!important;position:fixed!important;left:16px!important;bottom:16px!important;z-index:2147483647!important;display:block!important;';
    const root = host.attachShadow({ mode: 'open' });
    root.innerHTML = `
      <style>
        :host { color-scheme:dark; }
        * { box-sizing:border-box; }
        .panel { width:358px; max-width:calc(100vw - 32px); background:#151d2b;
          color:#edf3ff; border:1px solid #3e5068; border-radius:16px;
          box-shadow:0 12px 44px #0008; font:13px/1.45 system-ui,sans-serif; overflow:hidden; }
        header { padding:15px 16px 12px; background:#1e2b40; }
        .top { display:flex; align-items:center; justify-content:space-between; gap:8px; }
        strong { font-size:13px; letter-spacing:.05em; }
        .badge { color:#ffdc91; font-size:11px; font-weight:800; }
        .note { margin-top:5px; color:#c2cee1; font-size:11px; }
        .body { padding:12px 16px 16px; }
        .fields { display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; }
        label { display:block; font-size:11px; color:#c2cee1; }
        input { width:100%; margin-top:4px; padding:8px; background:#0d1522;
          color:#fff; border:1px solid #485971; border-radius:7px; font:inherit; }
        button { cursor:pointer; font:600 12px system-ui,sans-serif; border:1px solid #485971;
          border-radius:8px; background:#26354b; color:#edf3ff; padding:9px 10px; }
        button:hover { filter:brightness(1.15); }
        button:focus-visible, input:focus-visible, summary:focus-visible { outline:2px solid #96d4ff; outline-offset:2px; }
        button:disabled { opacity:.5; cursor:default; }
        #toggle { padding:3px 8px; font-size:16px; }
        .actions { display:flex; gap:8px; margin-top:10px; }
        .actions button { flex:1; }
        #up { background:#087144; border-color:#179b65; }
        #down { background:#304d7d; border-color:#537fb2; }
        #status { min-height:30px; margin-top:10px; color:#c2cee1; font-size:11px; }
        #trades { display:grid; gap:8px; max-height:280px; overflow:auto; margin:8px 0; }
        .trade { border:1px solid #19784d; background:#102c25; border-radius:10px; padding:10px; }
        .trade-top { display:flex; justify-content:space-between; gap:8px; }
        .value { color:#5aefa1; font-size:21px; font-weight:750; font-variant-numeric:tabular-nums; }
        .meta { color:#bad2c8; font-size:11px; }
        .tag { color:#ffd991; font-size:10px; font-weight:800; letter-spacing:.04em; }
        progress { width:100%; height:5px; accent-color:#3fe28c; display:block; margin-top:9px; }
        details { margin-top:10px; border-top:1px solid #34445c; padding-top:10px; }
        summary { cursor:pointer; color:#c2cee1; font-size:12px; }
        p { margin:8px 0; font-size:11px; color:#c2cee1; }
        .small { font-size:11px; padding:7px; }
        #clear { width:100%; margin-top:6px; }
        [hidden] { display:none!important; }
        .empty { color:#a9b9ce; padding:12px 0; text-align:center; }
        @media (prefers-reduced-motion:no-preference) { .trade { animation:appear .18s ease-out; } }
        @keyframes appear { from { opacity:0; transform:translateY(5px); } }
      </style>
      <section class="panel" aria-label="Симуляция сделок для видео">
        <header>
          <div class="top"><strong>ДЕМО / СИМУЛЯЦИЯ</strong><button id="toggle" aria-expanded="true" aria-label="Свернуть панель">−</button></div>
          <div class="note">Постановочные результаты для видео · не данные счёта</div>
        </header>
        <div class="body" id="body">
          <div class="fields">
            <label>Сумма, $<input id="amount" type="number" value="100" min="1" max="100000" step="0.01"></label>
            <label>Прибыль, %<input id="payout" type="number" value="92" min="1" max="100" step="0.01"></label>
            <label>Время, сек.<input id="seconds" type="number" value="10" min="1" max="3600" step="1"></label>
          </div>
          <div class="actions"><button id="up">↑ Сцена «Выше»</button><button id="down">↓ Сцена «Ниже»</button></div>
          <div class="actions"><button id="place-chart" class="small">Разместить на графике</button><button id="hide-chart" class="small">Скрыть график сцены</button></div>
          <div class="actions"><button id="auto-site" class="small" aria-pressed="true">Кнопки сайта: авто включено</button></div>
          <div id="status" role="status" aria-live="polite">«Купить/Продать» запускают сцену с параметрами сайта. Собственные кнопки используют поля выше.</div>
          <div id="trades" aria-label="Постановочные сделки"></div>
          <button id="clear" class="small">Очистить сцены</button>
          <details>
            <summary>Привязать к нажатию кнопок сайта</summary>
            <p>Выбери направление, затем нажми соответствующую кнопку сайта. Клик выбора будет перехвачен. Следующие клики выполняют обычное действие сайта и добавляют сцену здесь.</p>
            <div class="actions"><button id="bind-up" class="small">Выбрать «Выше»</button><button id="bind-down" class="small">Выбрать «Ниже»</button></div>
            <div class="actions"><button id="unbind" class="small">Убрать привязки</button><button id="cancel" class="small">Отмена выбора</button></div>
            <p>Ручные привязки используют поля этой панели. Для «Купить/Продать» приоритет имеет авто: параметры берутся с сайта. Ручные привязки действуют до перезагрузки; при замене кнопки выбери её заново. Esc отменяет выбор.</p>
          </details>
        </div>
      </section>`;
    document.body.append(host);

    const $ = id => root.getElementById(id);
    const money = value => '$' + value.toFixed(2);
    const directions = { up: '↑ Выше', down: '↓ Ниже' };
    const bindings = new Map();
    let autoSite = true;
    let selection = null;
    let counter = 0;
    let trades = [];
    const status = message => { $('status').textContent = message; };

    const chartHost = document.createElement('div');
    chartHost.id = 'po-video-chart';
    chartHost.style.cssText = 'all:initial!important;position:absolute!important;z-index:20!important;pointer-events:none!important;display:none!important;';
    const chart = chartHost.attachShadow({ mode: 'open' });
    chart.innerHTML = `
      <style>
        * { box-sizing:border-box; }
        section { position:relative; width:100%; height:100%; overflow:hidden;
          background:#101827; color:#dce9fa; font:12px/1.4 system-ui,sans-serif; }
        svg { display:block; width:100%; height:100%; }
        .label { position:absolute; left:12px; top:12px; color:#ffdc91; background:#101827dc;
          padding:4px 7px; font-size:11px; }
        .label small { display:block; color:#aabdce; font-size:10px; }
        .readout { position:absolute; left:12px; bottom:10px; display:flex; flex-wrap:wrap;
          gap:12px; padding:4px 7px; background:#101827dc; }
        #result { color:#5aefa1; font-weight:700; }
      </style>
      <section aria-label="График постановочной сцены">
        <div class="label">ДЕМО / СИМУЛЯЦИЯ<small>Условный индекс · не котировки</small></div>
        <svg viewBox="0 0 800 380" preserveAspectRatio="none" role="img" aria-label="Синтетическое движение цены выбранной сцены">
          <defs><pattern id="grid" width="80" height="55" patternUnits="userSpaceOnUse"><path d="M 80 0 L 0 0 0 55" fill="none" stroke="#253247" stroke-width="1"/></pattern></defs>
          <rect width="800" height="380" fill="url(#grid)"/>
          <path d="M 24 234 L 31 227 L 38 230 L 45 215 L 52 224 L 59 217 L 66 225 L 73 222 L 80 220" fill="none" stroke="#7999b5" stroke-width="2"/>
          <line x1="80" y1="220" x2="680" y2="220" stroke="#8193ae" stroke-dasharray="5 5"/>
          <text x="85" y="243" fill="#adbed3" font-family="sans-serif" font-size="13">Вход сцены · 100.000</text>
          <path id="trace" fill="none" stroke="#50e99c" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
          <line id="price-line" x1="80" x2="680" stroke="#50e99c" stroke-dasharray="3 4" opacity=".5"/>
          <circle id="dot" r="5" fill="#50e99c"/>
          <text id="price" x="690" fill="#50e99c" font-family="sans-serif" font-size="16"/>
        </svg>
        <div class="readout"><span id="scene">Создай сцену</span><span id="result"></span></div>
      </section>`;
    document.body.append(chartHost);
    const c$ = id => chart.getElementById(id);
    let chartTarget = null;
    let autoChart = false;
    const siteChart = () => document.querySelector('#chart-1 canvas.layer.plot') || document.getElementById('chart-1');
    let chartVisible = false;
    let chartScene = null;
    let mountedTarget = null;
    let mountedParent = null;
    let savedVisibility = null;
    let savedPosition = null;

    function restoreChart() {
        if (savedVisibility && mountedTarget) {
            if (savedVisibility.value) mountedTarget.style.setProperty('visibility', savedVisibility.value, savedVisibility.priority);
            else mountedTarget.style.removeProperty('visibility');
        }
        if (savedPosition && mountedParent) {
            if (savedPosition.value) mountedParent.style.setProperty('position', savedPosition.value, savedPosition.priority);
            else mountedParent.style.removeProperty('position');
        }
        savedVisibility = savedPosition = null;
        mountedTarget = mountedParent = null;
        chartHost.style.setProperty('display', 'none', 'important');
        // Keep our host out of subtrees that the site may replace.
        document.body.append(chartHost);
    }

    function mountChart(target) {
        const parent = target instanceof HTMLCanvasElement ? target.parentElement : target;
        if (!parent) return false;
        if (mountedTarget !== target || mountedParent !== parent || chartHost.parentElement !== parent) {
            restoreChart();
            mountedTarget = target;
            mountedParent = parent;
            if (getComputedStyle(parent).position === 'static' || !getComputedStyle(parent).position) {
                savedPosition = { value: parent.style.getPropertyValue('position'), priority: parent.style.getPropertyPriority('position') };
                parent.style.setProperty('position', 'relative', 'important');
            }
            if (target instanceof HTMLCanvasElement) {
                savedVisibility = { value: target.style.getPropertyValue('visibility'), priority: target.style.getPropertyPriority('visibility') };
                target.style.setProperty('visibility', 'hidden', 'important');
            }
            parent.append(chartHost);
        }
        return true;
    }

    function hideChart() {
        chartVisible = false;
        restoreChart();
    }

    function positionChart() {
        if (!chartVisible) return;
        if (autoChart) {
            chartTarget = siteChart();
            if (!chartTarget) {
                restoreChart();
                return;
            }
        }
        if (chartTarget && !chartTarget.isConnected) {
            chartTarget = null;
            hideChart();
            status('Сайт заменил область графика. Размести сцену заново.');
            return;
        }
        if (!chartTarget) {
            chartTarget = siteChart();
            autoChart = Boolean(chartTarget);
        }
        if (!chartTarget) {
            restoreChart();
            return;
        }
        const rect = chartTarget.getBoundingClientRect();
        if (rect && (rect.width < 240 || rect.height < 180 || rect.bottom <= 0 || rect.right <= 0 || rect.top >= innerHeight || rect.left >= innerWidth)) {
            restoreChart();
            return;
        }
        if (!mountChart(chartTarget)) return;
        const parentRect = mountedParent.getBoundingClientRect();
        const left = rect.left - parentRect.left + mountedParent.scrollLeft - mountedParent.clientLeft;
        const top = rect.top - parentRect.top + mountedParent.scrollTop - mountedParent.clientTop;
        const { width, height } = rect;
        for (const [key, value] of Object.entries({ left, top, width, height })) chartHost.style.setProperty(key, `${value}px`, 'important');
        chartHost.style.setProperty('display', 'block', 'important');
    }

    function updateChart(now) {
        if (!chartScene) return;
        const t = Math.min(1, Math.max(0, (now - chartScene.start) / chartScene.duration));
        const sign = chartScene.direction === 'up' ? 1 : -1;
        // Fixed, reproducible synthetic trajectory; its sign stays favorable after entry.
        const displacement = fraction => sign * (100 * fraction + 2 * Math.sin(fraction * Math.PI * 18) * Math.sin(Math.PI * fraction));
        const coordinates = fraction => [80 + fraction * 560, 220 - displacement(fraction)];
        const points = [];
        for (let i = 0; i <= Math.floor(t * 160); i++) points.push(coordinates(i / 160));
        points.push(coordinates(t));
        c$('trace').setAttribute('d', points.map(([x, y], i) => `${i ? 'L' : 'M'}${x.toFixed(2)} ${y.toFixed(2)}`).join(' '));
        const [x, y] = coordinates(t);
        c$('dot').setAttribute('cx', x);
        c$('dot').setAttribute('cy', y);
        c$('price-line').setAttribute('y1', y);
        c$('price-line').setAttribute('y2', y);
        c$('price').setAttribute('y', y + 5);
        c$('price').textContent = (100 + displacement(t) / 100).toFixed(3);
        c$('scene').textContent = `Сцена #${chartScene.id} · ${directions[chartScene.direction]} · ${t === 1 ? 'закрыта' : Math.ceil((chartScene.end - now) / 1000) + ' сек.'}`;
        c$('result').textContent = chartScene.value.textContent;
    }

    function settings() {
        const amount = Number($('amount').value);
        const payout = Number($('payout').value);
        const seconds = Number($('seconds').value);
        return validateSettings({ amount, payout, seconds });
    }

    function validateSettings({ amount, payout, seconds, asset = '' }) {
        if (!Number.isFinite(amount) || amount < 1 || amount > 100000 ||
            !Number.isFinite(payout) || payout < 1 || payout > 100 ||
            !Number.isInteger(seconds) || seconds < 1 || seconds > 3600) {
            status('Проверь поля: сумма 1–100 000 $, прибыль 1–100 %, время 1–3600 целых секунд.');
            return null;
        }
        return { amount: Math.round(amount * 100) / 100, payout, seconds, asset };
    }

    function readNumber(text) {
        const value = (text || '').trim().replace(/[\s\u00a0\u202f]/g, '');
        if (/^\d+(?:\.\d{1,2})?$/.test(value)) return Number(value);
        if (/^\d{1,3}(?:,\d{3})+(?:\.\d{1,2})?$/.test(value)) return Number(value.replace(/,/g, ''));
        if (/^\d+,\d{1,2}$/.test(value)) return Number(value.replace(',', '.'));
        return NaN;
    }

    function siteSettings(panel) {
        const expiration = panel.querySelector('.block--expiration-inputs');
        const time = expiration?.querySelector('.value__val')?.textContent.trim();
        const match = /^(\d{2}):([0-5]\d):([0-5]\d)$/.exec(time || '');
        if (!expiration?.querySelector('[data-src$="/exp-mode-1.svg"]') || !match) {
            status('Автосцена пропущена: нужен режим длительности ЧЧ:ММ:СС. Можно запустить сцену кнопками панели.');
            return null;
        }
        if (!panel.querySelector('.block--bet-amount .currency-icon--usd')) {
            status('Автосцена пропущена: валюта торговой панели должна быть USD.');
            return null;
        }
        const payoutText = panel.querySelector('.block--payout .value__val-start')?.textContent.trim() || '';
        const payoutMatch = /^\+?([\d.,\s]+)%$/.exec(payoutText);
        return validateSettings({
            amount: readNumber(panel.querySelector('.block--bet-amount input')?.value),
            payout: payoutMatch ? readNumber(payoutMatch[1]) : NaN,
            seconds: Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]),
            asset: panel.closest('.chart-item')?.querySelector('.current-symbol')?.textContent.trim() || ''
        });
    }

    function renderEmpty() {
        if (!trades.length) {
            const empty = document.createElement('div');
            empty.className = 'empty';
            empty.textContent = 'Сцен пока нет';
            $('trades').replaceChildren(empty);
        }
    }

    function updateTrade(trade, now) {
        const remaining = Math.max(0, trade.end - now);
        const fraction = Math.min(1, Math.max(0, 1 - remaining / trade.duration));
        // This is a scripted positive animation, not a quote or a platform P/L.
        const cents = remaining === 0 ? trade.profitCents :
            Math.max(1, Math.min(trade.profitCents, Math.round(trade.profitCents * (0.05 + 0.95 * fraction))));
        trade.value.textContent = '+' + money(cents / 100);
        trade.state.textContent = remaining === 0 ? 'Сцена закрыта в плюс' : `Симуляция · ${Math.ceil(remaining / 1000)} сек.`;
        trade.progress.value = fraction;
    }

    function addTrade(direction, override = null) {
        if (!allowed()) return;
        const config = override || settings();
        if (!config) return;
        if (trades.filter(t => t.end > Date.now()).length >= 20) {
            status('Уже запущено 20 сцен. Дождись закрытия или очисти список.');
            return;
        }
        if (!trades.length) $('trades').replaceChildren();
        const card = document.createElement('article');
        card.className = 'trade';
        card.innerHTML = '<div class="tag">ДЕМО / СИМУЛЯЦИЯ</div><div class="trade-top"><div><b class="name"></b><div class="meta settings"></div></div><div class="value"></div></div><div class="meta state"></div><progress max="1" aria-label="Время сцены"></progress>';
        card.querySelector('.name').textContent = `#${++counter} · ${directions[direction]}`;
        card.querySelector('.settings').textContent = `${money(config.amount)} · ${config.payout}%${config.asset ? ' · ' + config.asset : ''}`;
        const trade = {
            id: counter, direction, start: Date.now(),
            card, end: Date.now() + config.seconds * 1000, duration: config.seconds * 1000,
            profitCents: Math.round(config.amount * config.payout),
            value: card.querySelector('.value'), state: card.querySelector('.state'),
            progress: card.querySelector('progress')
        };
        trades.unshift(trade);
        // Retain every running scene; discard the oldest completed scenes first.
        while (trades.length > 20) {
            const old = trades.findLastIndex(t => t.end <= Date.now());
            if (old < 0) break;
            trades.splice(old, 1)[0].card.remove();
        }
        $('trades').prepend(card);
        updateTrade(trade, Date.now());
        chartScene = trade;
        chartVisible = true;
        positionChart();
        updateChart(Date.now());
        status(`Сцена #${counter} запущена. Результат задан сценарием.`);
    }

    function choose(direction) {
        selection = direction;
        status(`Нажми кнопку «${direction === 'up' ? 'Выше' : 'Ниже'}» на сайте. Esc — отмена.`);
    }

    function intercept(event) {
        if (!allowed() || event.composedPath().includes(host) || event.composedPath().includes(chartHost)) return;
        if (selection) {
            // Block the selection gesture before the site receives it.
            event.preventDefault();
            event.stopImmediatePropagation();
            if (event.type !== 'click') return;
            const target = event.target instanceof Element ? event.target : null;
            if (!target) return;
            if (selection === 'chart') {
                const rect = target.getBoundingClientRect();
                if (target === document.body || target === document.documentElement ||
                    target.contains(host) || target.contains(chartHost) || rect.width < 240 || rect.height < 180) {
                    status('Нажми на большую область самого графика (от 240 × 180). Esc — отмена.');
                    return;
                }
                chartTarget = target;
                autoChart = false;
                chartVisible = true;
                selection = null;
                positionChart();
                status('График симуляции размещён. Запусти сцену «Выше» или «Ниже».');
                return;
            }
            let control = target.closest('button, a, [role="button"], input[type="button"], input[type="submit"]');
            if (!control) {
                control = target;
                while (control.parentElement && control.parentElement !== document.body &&
                    getComputedStyle(control.parentElement).cursor === 'pointer') control = control.parentElement;
            }
            if (control === document.body || control === document.documentElement || control.contains(host)) {
                status('Выбери саму кнопку сайта. Esc — отмена.');
                return;
            }
            for (const [key, value] of bindings) if (value === control) bindings.delete(key);
            bindings.set(selection, control);
            status(`«${selection === 'up' ? 'Выше' : 'Ниже'}» привязана. Следующие нажатия добавят сцену.`);
            selection = null;
            return;
        }
        if (event.type !== 'click') return;
        const autoControl = event.target instanceof Element && event.target.closest(
            '#put-call-buttons-chart-1 .btn-call, #put-call-buttons-chart-1 .btn-put');
        if (autoSite && autoControl) {
            if (autoControl.closest(':disabled, [disabled], [aria-disabled="true"], .disabled')) return;
            const config = siteSettings(autoControl.closest('#put-call-buttons-chart-1'));
            if (config) {
                autoChart = true;
                chartTarget = siteChart();
                addTrade(autoControl.matches('.btn-call') ? 'up' : 'down', config);
            }
            return;
        }
        for (const [direction, control] of bindings) {
            if (control.isConnected && event.composedPath().includes(control)) {
                if (!control.matches(':disabled, [aria-disabled="true"]')) addTrade(direction);
                break;
            }
        }
    }

    const eventTypes = ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'touchstart', 'touchend', 'click'];
    const eventOptions = { capture: true, passive: false };
    for (const type of eventTypes) window.addEventListener(type, intercept, eventOptions);
    const keydown = event => {
        if (event.key === 'Escape' && selection) { selection = null; status('Выбор отменён.'); }
    };
    window.addEventListener('keydown', keydown, true);
    $('up').onclick = () => addTrade('up');
    $('down').onclick = () => addTrade('down');
    $('place-chart').onclick = () => {
        hideChart();
        selection = 'chart';
        status('Нажми на область графика сайта. Здесь появится график симуляции. Esc — отмена.');
    };
    $('hide-chart').onclick = hideChart;
    $('bind-up').onclick = () => choose('up');
    $('bind-down').onclick = () => choose('down');
    $('cancel').onclick = () => { selection = null; status('Выбор отменён.'); };
    function setAutoSite(enabled) {
        autoSite = enabled;
        $('auto-site').textContent = `Кнопки сайта: авто ${enabled ? 'включено' : 'выключено'}`;
        $('auto-site').setAttribute('aria-pressed', String(enabled));
    }
    $('auto-site').onclick = () => {
        setAutoSite(!autoSite);
        status(autoSite ? 'Автосцены для «Купить/Продать» включены.' : 'Автосцены выключены. Ручные привязки действуют, если заданы.');
    };
    $('unbind').onclick = () => { bindings.clear(); selection = null; setAutoSite(false); status('Привязки удалены, автосцены выключены.'); };
    $('clear').onclick = () => { trades = []; chartScene = null; hideChart(); $('trades').replaceChildren(); renderEmpty(); status('Сцены очищены.'); };
    $('toggle').onclick = () => {
        $('body').hidden = !$('body').hidden;
        if ($('body').hidden) selection = null;
        $('toggle').textContent = $('body').hidden ? '+' : '−';
        $('toggle').setAttribute('aria-expanded', String(!$('body').hidden));
        $('toggle').setAttribute('aria-label', $('body').hidden ? 'Развернуть панель' : 'Свернуть панель');
    };
    renderEmpty();

    const timer = window.setInterval(() => {
        if (!allowed() || !host.isConnected) {
            window.clearInterval(timer);
            for (const type of eventTypes) window.removeEventListener(type, intercept, eventOptions);
            window.removeEventListener('keydown', keydown, true);
            restoreChart();
            host.remove();
            chartHost.remove();
            return;
        }
        for (const [direction, control] of bindings) {
            if (!control.isConnected) {
                bindings.delete(direction);
                status('Сайт заменил привязанную кнопку. Выбери её заново.');
            }
        }
        const now = Date.now();
        for (const trade of trades) updateTrade(trade, now);
        if (chartVisible) { positionChart(); updateChart(now); }
    }, 100);
})();