Датасет и скрипты опубликованы на HuggingFace

У TradingView есть портал Ideas. Это система личных блогов трейдеров, куда они постят свои догадки. Ценность информации в том, что такая догадка сразу содержит направление: SHORT или LONG. Это позволяет посчитать настроения на рынке

Я посмотрел, заметил что API не сложное, легко сдампить. Делюсь результатами анализа
Инвестиция глазами бизнеса: четыре оси
Прежде чем спрашивать «хорош ли аналитик», надо определить, что такое инвестиция с точки зрения управления рисками. Ответ раскладывается на четыре величины:
сколько времени заморожены деньги — максимальное удержание позиции;
какую просадку инвестор готов терпеть — глубина жёсткого стопа;
какая прибыль считается достаточной — уровень фиксации (profit lock);
сколько отдать от пика при развороте — откат трейлинга.
Откуда взять вход в позицию из этой догадки
Все гениальное просто: перебрать грубой силой. Каждая комбинация этих четырёх величин это отдельная стратегия чтения одного и того же прогноза.
hardStopPercent: 1 … 10 % (19 значений) trailingTakePercent: 0.5 … 5 % (10 значений) holdMinutes: 1 … 14 суток (14 значений) profitLockPercent: 1 … 5 % (8 значений) = 21 280 точек
После перебора мы получим Positive и False Positive вперемежку в одной выборке. Они идут кластером по sharpe ratio, остальные это промахи, не являются входами вовсе. Торговать на этом не получится, так как мы не знаем точно какой из вариантов эффективного входа выбрал автор, но это позволяет оценить догадку по существу: а могло ли оно вообще принести деньги
Неудачные попытки
Прежде чем дойти до работающего отбора, я сделал полтора десятка попыток. Ниже примечательные из них чтобы быстро ввести в контекст как ведет себя толпа
Отбор авторов по прошлому результату
По hitRate прошлого месяца. Чистая регрессия к среднему — прошлая точность не содержит информации о будущей.
Пороговые критерии. Защита от малой выборки честно работает и потому надёжнее всех находит авторов на пике удачи, но прямо перед регрессией.
Удлинение трека до 2/3/6 месяцев. Тенденция к улучшению есть, ноль не пересекается ни разу.
Отбор внутри одного символа. Минус на всех четырёх проверенных символах.
Score = число удачных месяцев. Премирует долгожителей, а не точных: не хватит никакой выборки сделать и backtest и paper, 64 месяца мало
«Ни одного провального месяца». Единственная статистически значимая серия (21 месяц подряд) принесла копейки, а серия в 15 месяцев у другого автора закончилась убытком. Серии не переводятся в деньги.
Персональные настройки под автора
Своё окно риска по hitRate. Оптимизатор у всех авторов уезжает в один угол сетки (трейлинг 0.5 %, лок 1 %) — тот самый, где hitRate ≈ 1.0 при убытке. Единое окно даёт всем авторам столько же плюсовых месяцев, сколько «персональные».
Своё окно по PnL. Учебная иллюстрация подгонки, окно зависит от режима рынка и не переносится между месяцами по автору
Толпа как сигнал
Консенсус направлений. Согласие авторов в окне ±24 часа выглядело рабочим фильтром — пока окно согласия было симметричным и включало посты, опубликованные после входа. Причинное окно (только прошлое) убивает эффект полностью: улучшение целиком создавали идеи из будущего, которых на момент сделки не существовало.
Кластеры постов как ранний сигнал. Вход в начале кластера ≈ вход по посту (разница в сотые доли процента на сделку). Кластеры, сдвинутые на случайные ±30 суток, зарабатывают столько же; кластеры, собранные из постов по чужим монетам, — даже больше настоящих. Плотность постов ловит общий дрейф рынка, а не вброс в конкретную монету.
Поиск бот-сетей. Посты кучкуются не потому, что авторы переписывают друг друга, а потому что хором реагируют на уже случившееся движение. Прямой замер: тот же набор идей со входом на неделю раньше публикации был бы в 3–5 раз выгоднее — во всех 100 розыгрышах из 100. Пост стоит в конце движения, а не в начале.
Общий знаменатель всех провалов один: рейтинг по прошлому результату умеет находить плохих авторов и не умеет находить хороших. Худшие по любому рейтингу стабильно хуже случайного выбора; лучшие — не лучше него ни разу.
Итого: Правило исполнения
Работа по новостям это Swing Trading. Позиции открываются на срок 14 дней. Обычный стоп лосс считается через внутридневную волатильность. Двухнедельная идея не имеет отношения: для нее ставится хард стоп - статистически недостижимое условие черного лебедя
вход строго по публикации поста доверенного автора, ждать нельзя (тот же набор идей со входом на 1–30 суток позже даёт около нуля) сторона только LONG, только спот холд 14 суток — это и есть фиксация убытка: не отработала — выходим профит-лок 20 % трейлинг 8 % стоп статистически недостижим: предохранитель от чёрного лебедя, не инструмент управления позицией
Ограничение жёсткое: конструкция реализуема только на споте без плеча, и размер позиции обязан считать глубокую просадку одной сделки допустимым исходом, а не «маловероятным». Убыток фиксирует таймер. Роли выходов в этой конструкции распределены так:
профит-лок и трейлинг — фиксация прибыли, они и создают эдж;
истечение холда (14 суток) — штатная фиксация убытка: идея не отработала за свой горизонт — выходим, не дожидаясь «отскока»;
стоп — предохранитель от чёрного лебедя, поставленный статистически недостижимо для рабочих движений рынка. Его срабатывание — не торговое событие, а авария.
Кто такой лидер мнений на самом деле
Итог переворачивает само определение. Лидер — это не автор с лучшим треком: любой рейтинг по прошлому результату не отличим от случайного выбора или хуже него. Лидер — это тот, кто пишет, когда по монете ещё тихо: не больше двух чужих постов по символу за сутки до его публикации.
автор |
идей за окно |
доля ранних |
роль |
|---|---|---|---|
coinpediamarkets |
78/год |
60 % |
доверенный №1 |
DEXWireNews |
15 |
60 % |
доверенный №2 |
Склонность писать в тишине оказалась самым устойчивым свойством автора в датасете. И отбор по ней работает вперёд, walk-forward, без подглядывания в результаты: топ-2 автора по доле ранних постов за прошлые 12 месяцев бьёт случайный выбор из тех же допущенных
автор |
идей всего |
доля ранних |
PnL/сд ранних |
PnL/сд поздних |
PnL/сд всего |
|---|---|---|---|---|---|
coinpediamarkets |
189 |
76 % |
+3.861 |
−0.387 |
+2.850 |
DEXWireNews |
91 |
57 % |
+15.003 |
+0.990 |
+8.997 |
Пост считается ранним, если за 24 часа до его публикации по тому же символу вышло не больше 2 чужих постов. Лента для подсчёта — все посты, включая NEUTRAL (нейтральные не торгуются, но шум в ленте создают). То есть «ранний» = «написал в тишину», а не «написал первым в цепочке». Никакого кластера, к которому надо привязаться, не нужно — у каждого поста своя скользящая 24-часовая ретроспектива, и вопрос continuous-ленты снимается: ранним может быть любой пост в любой момент, если вокруг него было тихо.
Проверка живьём: paper trading
Задача стенда — одна: подтвердить или опровергнуть гипотезу о лидере мнений на практике. Поэтому бэктест уходит на стенд, который торгует правило на бумаге в реальном времени, — исполнение заморожено, проверяется только отбор. Крутить исполнение параллельно значило бы проверять две гипотезы сразу.
Параметры стенда:
отбор: топ-2 автора по доле ранних постов за скользящие 12 месяцев, допуск ≥ 10 идей за окно — допуск заодно гарантирует, что автор жив и пишет;
вход: строго по публикации поста доверенного автора, в его сторону, без ожидания;
исполнение: холд 14 суток, профит-лок 20 %, трейлинг 8 %, стоп статистически недостижим — только LONG, только спот;
символ: пока BTCUSDT;
пересчёт доверенных: ежемесячно;
скрейпер: опрашивает ленты каждые 10 минут и пишет per-idea
Ожидаемый темп — единицы сделок в месяц: правило редкое, недели тишины — норма. Забавная деталь про выбор окна отбора: бектест выводит в топ авторов, которые давно перестали писать. Короткое окно с порогом активности решает это само. Лучшая статистика выбрала мёртвых; прод выбирает живых.
Код отбора ранних авторов для копипасты
function pickEarlyAuthors(ideas, testMonth, K = 2, EARLY_MAX = 2, WIN_H = 24) { const from = monthAdd(testMonth, -12), to = testMonth; const bySym = groupBy(ideas, (i) => i.symbol); // ВСЕ посты, и NEUTRAL const stat = {}; // author -> [ранних, всего] for (const list of Object.values(bySym)) { list.sort((a, b) => a.ts - b.ts); let lo = 0; list.forEach((idea, hi) => { if (idea.ts < from || idea.ts >= to || idea.direction === "NEUTRAL") return; while (list[lo].ts < idea.ts - WIN_H * 3600e3) lo++; let others = 0; for (let j = lo; j < hi; j++) if (list[j].author !== idea.author) others++; const s = (stat[idea.author] ??= [0, 0]); s[0] += others <= EARLY_MAX ? 1 : 0; s[1] += 1; }); } return Object.entries(stat) .filter(([, s]) => s[1] >= 10) .sort((a, b) => b[1][0] / b[1][1] - a[1][0] / a[1][1]) .slice(0, K) .map(([author]) => author); }
Спасибо за внимание!
Комментарии (12)

tripolskypetr Автор
30.07.2026 11:06Код для копипасты поиска первого блогера в серии
⠀
function pickEarlyAuthors(ideas, testMonth, K = 2, EARLY_MAX = 2, WIN_H = 24) { const from = monthAdd(testMonth, -12), to = testMonth; const bySym = groupBy(ideas, (i) => i.symbol); // ВСЕ посты, и NEUTRAL const stat = {}; // author -> [ранних, всего] for (const list of Object.values(bySym)) { list.sort((a, b) => a.ts - b.ts); let lo = 0; list.forEach((idea, hi) => { if (idea.ts < from || idea.ts >= to || idea.direction === "NEUTRAL") return; while (list[lo].ts < idea.ts - WIN_H * 3600e3) lo++; let others = 0; for (let j = lo; j < hi; j++) if (list[j].author !== idea.author) others++; const s = (stat[idea.author] ??= [0, 0]); s[0] += others <= EARLY_MAX ? 1 : 0; s[1] += 1; }); } return Object.entries(stat) .filter(([, s]) => s[1] >= 10) .sort((a, b) => b[1][0] / b[1][1] - a[1][0] / a[1][1]) .slice(0, K) .map(([author]) => author); }Код для копипасты брутфорса входов на любой выборке
⠀
import { addExchangeSchema, addSweepSchema, Sweep } from "backtest-kit"; import { readFileSync, writeFileSync } from "fs"; import { singleshot } from "functools-kit"; import { createWriteStream } from "fs"; import { once } from "events"; import ccxt from "ccxt"; // result_reports.jsonl на полной сетке — гигабайты на символ-месяц, // JSON.stringify всего массива не влезет в кучу: пишем потоком async function writeLines(path, arr) { const ws = createWriteStream(path, { flags: "w", encoding: "utf8", highWaterMark: 1024 * 1024, }); for (let i = 0; i < arr.length; i++) { if (!ws.write(JSON.stringify(arr[i]) + "\n")) { await once(ws, "drain"); } arr[i] = null; // освобождаем ссылку } ws.end(); await once(ws, "finish"); } const getExchange = singleshot(async () => { const exchange = new ccxt.binance({ options: { defaultType: "spot", adjustForTimeDifference: true, recvWindow: 60000 }, enableRateLimit: true, timeout: 15000, }); await exchange.loadMarkets(); return exchange; }); addExchangeSchema({ exchangeName: "ccxt_cached", getCandles: async (symbol, interval, since, limit) => { const exchange = await getExchange(); const candles = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit); return candles.map(([timestamp, open, high, low, close, volume]) => ({ timestamp, open, high, low, close, volume, })); }, }); addSweepSchema({ sweepName: "tv_probe", exchangeName: "ccxt_cached", reportOrder: "sharpe", gridAxes: { hardStopPercent: [ 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10, ], trailingTakePercent: [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5], holdMinutes: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, ].map((d) => d * 24 * 60), profitLockPercent: [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0], }, callbacks: { onProgress(symbol, stage, processed, total) { console.log(`symbol=${symbol} stage=${stage} processed=${processed} total=${total}`); }, }, }); const ideas = readFileSync("./assets/tv-ideas.normalized.jsonl", "utf-8") .split("\n") .filter(Boolean) .map((line) => JSON.parse(line)); const result = await Sweep.run({ symbol: "BTCUSDT", sweepName: "tv_probe", ideas, }); const { reports, ...other } = result; writeFileSync("./result.json", JSON.stringify(other, null, 2)); await writeLines("./result_tracks.jsonl", reports.tracks); await writeLines("./result_best.jsonl", reports.best); await writeLines("./result_reports.jsonl", reports.reports); process.exit(0);

tripolskypetr Автор
30.07.2026 11:06Другими словами, после того как на рынке проливается красное, тот кто первый заорет
тот и правстановится The One в оккультном понимании:
А вот в какого бога они верят и кто его мессия, тут наверное надо Арию слушать. Вот этот заоравший первый, он же единственный. Но только по тому что после после резни живым остался

house2008
30.07.2026 11:0614 суток — это и есть фиксация убытка: не отработала — выходимЯ надеюсь это шутка, ведь на 15й день может быть туземун) С таким риск менеджментом всё сливается на длинной дистанции 100%.
Описание похоже на своего рода инвестирование, причем сброс позиции происходит наихудшим образом на 14 день, если просели на 50% то уже логично пересидеть так как спот, иначе нужно было сбрасывать когда получили посадку на 10%.
Свинг спотом, этот стиль хорошо подходит под торговлю по объемным уровням на 4H. Знаю (не лично) профитного трейдера, он все записи ведет тоже на TV чтобы народ повторял если хочет, там как раз сделки раз в неделю или реже если мало активов будете использовать. Если нужно ссылку на ТВ, напишите в ЛС.
пс. Я не трейдер, просто интересна тема трейдинга

tripolskypetr Автор
30.07.2026 11:06Тут не совсем туземун, критерий закрытия же это не цена, а время
Но если случился туземун сразу надо высовывать

house2008
30.07.2026 11:06Если делать трейдинг по таймингу, я бы лучше сделал вход в позицию на 2й-3й час Нью Йорк сессии, много раз замечал, что первые часы задают тренд всему дню. Более того, многие трейдеры так и делают, после открытия Америки в первый часы открывают позиции так как волатильность огромная, главное угадать с трендом.

tripolskypetr Автор
30.07.2026 11:06Этот датасет нужен чтобы озадачить ИИ агента: вот пусть крутится и перебирает варианты
import { addStrategySchema, listenError, listenActivePing, commitTrailingTakeCost, getDate, getAveragePrice, Log, listenIdlePing, alignToInterval, getClosePrice, listenSignal, Position, commitClosePending, getPositionHighestProfitDistancePnlPercentage, getPositionHighestPnlPercentage, getPositionPnlPercent, getCandles, getPositionHighestProfitDistancePnlCost, getPositionHighestMaxDrawdownPnlCost, getPositionPnlCost, getPositionHighestPnlCost, getPositionHighestProfitMinutes, } from "backtest-kit"; import { errorData, getErrorMessage, randomString, str } from "functools-kit"; import { readFileSync } from "fs"; import { SignalEntryModel } from "./model/SignalEntry.model"; const PEAK_STALENESS_SINCE_PROFIT = 1.0; const PEAK_STALENESS_SINCE_MINUTES = 240; const TRAILING_TAKE = 1.0; const HARD_STOP = 1.0; const SIGNALS: SignalEntryModel[] = readFileSync( "./assets/entry.jsonl", "utf-8", ) .split("\n") .filter(Boolean) .map((line) => JSON.parse(line)); function getActiveSignal(symbol: string, when: Date): SignalEntryModel | null { const now = when.getTime(); const match = SIGNALS.find( (s) => { if (s.symbol !== symbol) { return false; } const publishedAt = alignToInterval(new Date(s.publishedAt), "1m"); return publishedAt.getTime() === now; } ); return match ?? null; } addStrategySchema({ strategyName: "jan_2026_strategy", getSignal: async (symbol, when, currentPrice) => { console.log(when) const signal = getActiveSignal(symbol, when); if (!signal) { return null; } const close_1m = await getClosePrice(symbol, "1m"); if (close_1m < signal.entry.from || close_1m > signal.entry.to) { return null; } return { ...Position.moonbag({ position: signal.direction, currentPrice, percentStopLoss: HARD_STOP, }), minuteEstimatedTime: 24 * 60, note: signal.note, }; }, }); listenActivePing(async ({ symbol, data }) => { const peakProfitDistance = await getPositionHighestProfitDistancePnlPercentage(symbol); const currentProfit = await getPositionPnlPercent(symbol); if (currentProfit < 0) { return; } if (peakProfitDistance < TRAILING_TAKE) { return; } Log.info("position closed due to the trailing take", { symbol, data, }); await commitClosePending(symbol, { id: "unknown", note: str.newline( "# Позиция закрыта по trailing take", ), }); }); listenActivePing(async ({ symbol, data }) => { const peakProfitCost = await getPositionHighestPnlPercentage(symbol); const peakProfitMinutes = await getPositionHighestProfitMinutes(symbol); if (peakProfitCost < PEAK_STALENESS_SINCE_PROFIT) { return; } if (peakProfitMinutes < PEAK_STALENESS_SINCE_MINUTES) { return; } Log.info("position closed due to the peak staleness", { symbol, data, }); await commitClosePending(symbol, { id: "unknown", note: str.newline( "# Позиция закрыта по peak staleness", ), }); }); listenError((error) => { console.log(error); Log.debug("error", { error: errorData(error), message: getErrorMessage(error), }); });В этой же экосистеме есть среда исполнения, которая функционально чисто позволяет кодить сложные условия выхода, знать бы что

tripolskypetr Автор
30.07.2026 11:06Прибыль может быть не 20, а 12 процентов. Если от них откатилось уже 8, то нужно выходить на +4, так как статистически это разворот

house2008
30.07.2026 11:06Если от них откатилось уже 8, то нужно выходить на +4, так как статистически это разворот
Или свип ликвидности, ловушка, pullback, после чего тренд продолжится с большей силой

tripolskypetr Автор
30.07.2026 11:06Эффективно определить это по режиму рынка можно только статистически на большой выборке.

house2008
30.07.2026 11:06Нет, рынок меняется, вы не успеете накопить статистику потому что к этому времени она уже будет невалидна.
Тот же биткоин, статистически по всем циклам топ рынка в этом цикле должен был быть 180к-220к минимум, но мы видим что произошло, это слишком очевидно.

tripolskypetr Автор
30.07.2026 11:06Речь идет об одной позиции. Если для одной позиции не работает статистика, то невозможен риск менеджемент как класс
tripolskypetr Автор
⠀
⠀
⠀