Перейти к содержимому

Свой скринер

Скринер — небольшая библиотека на Rust или модуль на TypeScript, собранные в WebAssembly. Терминал запускает плагин в песочнице: своей сети, файлов и часов у него нет. Данные он получает от терминала — тикеры, списки инструментов, HTTPS-запросы к API бирж из разрешённого списка, облачную историю — и возвращает строки таблицы. Клик по строке открывает стакан её инструмента.

Всё для разработки собрано в открытом SDK space-screener-sdk: утилита командной строки st, библиотека для Rust, шаблоны проектов и шесть примеров. Страница техническая: нужны командная строка и базовое знание Rust или TypeScript. Не хотите писать код сами — поручите это Claude: Скринер с помощью Claude.

  • Space Terminal 0.104.71 или новее, запущенный на этом компьютере, с подключёнными биржами. Сервер Локальный API должен быть включён (по умолчанию так и есть, см. Локальный API): через него st ставит плагин в терминал.
  • Rust 1.91 или новее с cargo — установка на rustup.rs, обновление — rustup update. Нужен и для TypeScript: st ставится через cargo.
  • Node.js 18 или новее — только для скринеров на TypeScript.
Окно терминала
cargo install --git https://github.com/Space-Terminal/space-screener-sdk st
st --version

Цель сборки wasm32-unknown-unknown st добавит сам через rustup при первой сборке. Для TypeScript st при первой сборке один раз запустит npm install в проекте и скачает сборщики extism-js и binaryen, проверив их контрольные суммы.

Окно терминала
st init my-screener --id you.my-screener
cd my-screener

--id — имя скринера, уникальное во всём каталоге: латиница в нижнем регистре, цифры, ., _ и -, от 3 до 64 символов, начинается и заканчивается буквой или цифрой. Принято писать автор.название. Без --id получится local.<папка>. Имена install и sync заняты. Для TypeScript добавьте --lang ts.

Что появится в папке:

ФайлЧто в нём
manifest.yamlОписание скринера: имя, колонки, параметры, адреса, частота обновления
src/lib.rsКод на Rust. Шаблон — рабочий скринер «самые сильные движения за сутки» по подключённым фьючерсным биржам
Cargo.tomlПроект Rust, уже настроенный на сборку в wasm
src/index.ts, package.json, tsconfig.jsonТо же для TypeScript (--lang ts)
pdk/Только TypeScript: библиотеку плагина пишет и обновляет st — не правьте её

Фрагмент manifest.yaml, который создаёт st init:

abi: 1
id: you.my-screener
version: 0.1.0
name: {ru: "Мой скринер", en: "My screener"}
lang: rust
min_terminal: 0.104.71
http: [] # адреса для HTTPS-запросов, например [fapi.binance.com]
timer_ms: 10000 # как часто вызывать on_timer, мс
columns:
- {key: symbol, type: symbol, title: {ru: "Тикер", en: "Symbol"}}
- {key: exchange, type: exchange, title: {ru: "Биржа", en: "Exchange"}}
- {key: change, type: percent, title: {ru: "Изм. 24ч", en: "24h change"}, sort: desc}
- {key: volume, type: usd, title: {ru: "Объём 24ч", en: "24h volume"}}
params:
- {key: min_volume, type: number, title: {ru: "Мин. объём, $", en: "Min volume, $"}, default: 1000000, min: 0}
ПолеОбязательноеЧто задаёт
abiдаВерсия контракта с терминалом, всегда 1
idдаИмя скринера — см. выше
versionдаВерсия вида 1.2.0. Для каталога — без хвоста +…
nameдаНазвание на русском и английском
langдаrust или ts
min_terminalдаМинимальная версия терминала. st init пока подставляет 0.104.70 — поставьте 0.104.71: плагины появились в этой версии
httpнетТочные адреса, к которым плагин ходит по HTTPS: fapi.binance.com, без https://, без IP-адресов и localhost. К остальным запрос не пройдёт
timer_msнетПауза между вызовами on_timer: от 250 мс до часа, по умолчанию 1000
columnsдаКолонки таблицы
paramsнетПараметры, которые пользователь меняет в окне параметров — шестерёнка в шапке панели
limitsнетmemory_mb — до 64, cpu_ms_per_call — от 50 до 1000 мс процессора на вызов, по умолчанию 250
historyнетДоступ к облачной истории: cluster — кластеры, replay — запись ленты и стакана
signalsнетСигналы сервера Space: activity — активность инструментов, density — плотности, prints — серии одинаковых сделок. Терминал отдаёт только density. Нужен min_terminal: 0.104.72 — см. Сигналы Space
descriptionдля каталогаТекст карточки на русском и английском, до 2000 символов
categoriesдля каталогаОт одной до трёх категорий — таблица ниже
sourceнетСсылка https:// на исходный код

Колонка — {key, type, title, sort, width, show_if}. key — имя ячейки в строках, title — заголовок на двух языках, sort: desc или asc у первой такой колонки задаёт сортировку по умолчанию, width — ширина в пикселях, от 1 до 2000. Ключ колонки не может повторяться.

show_if — ключ параметра типа bool: пока он выключен, терминал прячет колонку, включили — колонка вернулась, без переустановки. Так делается флажок «показывать колонку» в окне параметров:

columns:
- {key: chg5, type: percent, title: {ru: "OI 5м", en: "OI 5m"}, show_if: col_chg5}
params:
- {key: col_chg5, type: bool, title: {ru: "Колонка OI 5м", en: "OI 5m column"}, default: true}

Если show_if ссылается не на bool-параметр, манифест не пройдёт проверку (invalid_manifest у columns[i].show_if). Терминал 0.104.73 и новее прячет такие колонки, более старые показывают их всегда — новый min_terminal не нужен. Ячейки скрытой колонки строки могут присылать и дальше. Кроме того, пользователь сам скрывает любые колонки в окне параметров — это настройка панели, плагину она не передаётся.

Тип колонкиЧто показывает
textТекст
number, integerЧисло, целое число
percentПроценты — значение уже в процентах: 1.5 — это 1,5 %
usd, priceСумма в долларах, цена
timeВремя — миллисекунды с 1970 года
durationДлительность в миллисекундах
countdownОбратный отсчёт до момента — миллисекунды с 1970 года
symbol, exchangeТикер, биржа
exchangesНесколько бирж со значками спота и фьючерсов
boolДа или нет

Параметр — {key, type, title, default, min, max, options}. Типы: number, integer, bool, text (до 256 символов) и select — выбор из options. default обязателен и должен подходить под тип и границы.

Категории для каталога — значения categories и как они называются на витрине:

ЗначениеНа витрине
volumeОбъём
open-interestОткрытый интерес
fundingФандинг
spreadСпред
moversДвижения цены
listingsЛистинги
orderbookСтакан
otherДругое

Скринер на Rust — структура с методом on_timer, который терминал вызывает раз в timer_ms: собрать данные, посчитать, отдать строки. Упрощённый шаблон — в полном ошибка одной биржи не останавливает остальные, а строки сортируются и обрезаются по параметру:

use space_screener::prelude::*;
#[derive(Default)]
struct MyScreener;
impl Screener for MyScreener {
fn on_timer(&mut self, _now_ms: i64) -> ScreenerResult {
let min_volume = params().f64_or("min_volume", 1_000_000.0);
let mut rows = Vec::new();
for ex in exchanges()? {
if !ex.connected || ex.market != Market::Futures {
continue;
}
for t in tickers(&ex.exchange, ex.market)?.tickers {
if t.volume_quote < min_volume {
continue;
}
rows.push(
Row::new(format!("{}:{}", ex.exchange, t.symbol))
.market_ref(&MarketRef::new(&ex.exchange, ex.market, &t.symbol))
.cell("symbol", t.symbol.as_str())
.cell("exchange", ex.exchange.as_str())
.cell("change", Cell::signed(t.change_pct))
.cell("volume", t.volume_quote),
);
}
}
replace_rows(rows)?;
Ok(())
}
}
export_screener!(MyScreener);
  • Данные. exchanges() — биржи, которые подключил пользователь; tickers() — суточные тикеры терминала; symbols() — список инструментов; http() и http_batch() — HTTPS-запросы к адресам из http манифеста, http_batch() отправляет много запросов параллельно.
  • Строки. Row::new(ключ) — ключ строки уникален и стабилен; .cell(колонка, значение) — ячейки, ключи должны быть колонками манифеста; Cell::signed() красит значение в зелёный или красный по знаку. replace_rows() заменяет всю таблицу, emit_rows() обновляет строки по ключу.
  • Порядок и избранное. .rank(n) — порядок строк внутри данных плагина: строки с большим rank стоят выше при любой сортировке — например, закреплённые плагином строки сверху, чёрный список внизу. Избранное тикеров делает терминал: он сам рисует ★ в каждом скринере и держит избранные тикеры выше всех остальных строк, так что своя колонка избранного плагину не нужна. Фильтры Биржи и Секторы терминал тоже применяет сам — по бирже, рынку и тикеру строки.
  • Клик. Строка с .market_ref(…) по клику сама открывает стакан своего инструмента — писать для этого код не нужно. Открыть стакан или стакан с графиком, заменить открытые панелью стаканы или добавить колонку — выбирает пользователь в блоке Клик по тикеру окна параметров. Стрелки ↑/↓ в панели кликают выбранную строку: плагин с on_click получает такой же вызов, как от левого клика мышью вне колонки, — column пустой, без модификаторов.
  • Статус и сигналы. set_status() — короткая строка статуса в подсказке у точки в шапке панели. alert() — всплывающее сообщение и запись в колокольчике, не чаще 6 в минуту.
  • Параметры и память. params() — текущие значения параметров. Состояние в памяти пропадает при перезапуске плагина; то, что должно его пережить, сохраняйте через kv_set() — до 1 МиБ.
  • Лог. info!, warn! и error! пишут в лог плагина — его показывает st logs.

Полный список функций, пример открытого интереса по восьми биржам и все тонкости — в SKILL.md и ABI.md, готовые проекты — в examples: открытый интерес по восьми биржам, самые сильные движения, ставки фандинга, спред одной пары между биржами, плотности из сигналов Space и всплески объёма на TypeScript.

На TypeScript функции те же: exchanges(), tickers(), replaceRows(), setStatus(), параметры — num("min_volume", 1000000). Две особенности: код на верхнем уровне модуля выполняется один раз при сборке, поэтому обращайтесь к терминалу только внутри init и on_timer; TypeScript-плагин работает заметно медленнее такого же на Rust.

Сервер Space сам считает сигналы по всем биржам: крупные плотности в стаканах, активность инструментов и серии одинаковых сделок. Терминал держит к нему живое подключение, а плагин получает эти сигналы от терминала — без своих запросов в сеть. Так устроен скринер Space Density; на тех же сигналах можно сделать свой.

  • Манифест. Перечислите нужные источники в signals, например signals: [density], и поставьте min_terminal: 0.104.72 — более старый терминал такой плагин не поставит.
  • Данные. SignalFeed::new(SignalSource::Density) держит текущий набор сигналов; poll() в каждом on_timer подтягивает изменения с прошлого вызова, iter() перебирает сигналы, connected() говорит, свежие ли данные. Числа в сигналах — для отображения.
  • Доступность источников. Терминал отдаёт density. Источники activity и prints он не отдаёт — signals отвечает ошибкой unavailable, покажите пользователю статус через set_status(). Если источник не перечислен в signals манифеста, ошибка будет другая — not_permitted.
  • Клик. Свой обработчик клика — метод on_click и export_screener!(MyScreener, on_click). В нём доступны ещё два действия, одно на клик: open_markets(&[…]) открывает до 16 стаканов разом, open_market_with_level(&market, &SmartLevel::new(id)) открывает стакан и ставит умный уровень на цене плотности с этим id — точную цену и метрики терминал берёт из своих данных.

Готовый пример — examples/density, формат сигналов и все поля — в разделе v1.1 ABI.md.

Окно терминала
st build # собрать screener.wasm и проверить его по правилам терминала
st validate # проверить манифест и уже собранный модуль

st build проверяет модуль так же, как это сделает терминал, и печатает все ошибки. Строка catalog: ready в выводе st validate значит, что заполнены поля для каталога.

  1. Запустите терминал и подключите биржи, данные которых нужны скринеру.
  2. В папке проекта выполните st dev. Утилита соберёт плагин, поставит его в терминал, и терминал сам откроет вкладку с его панелью. st dev остаётся работать: при каждом сохранении файла плагин пересобирается и переставляется, лог плагина идёт в консоль. Флаг --no-open — не открывать панель.
  3. Проверьте таблицу в панели и кликните строку — должен открыться стакан.
  4. В соседнем окне консоли: st rows — строки так, как их видит терминал; st logs -f — лог плагина; st list — все установленные плагины и их статус.

Плагины, поставленные через st dev, видны в списке выбора панели Скринеры под разделителем Space Market вместе с каталожными. Скринер из каталога с тем же именем терминал поверх вашей версии не ставит.

st находит терминал по файлу screeners/local_api_token в папке данных терминала и берёт порт из его настроек, по умолчанию 5055. Папка данных — ~/Library/Application Support/Space Terminal на macOS, %APPDATA%\Space Terminal на Windows, ~/.local/share/Space Terminal на Linux. Другую папку задаёт переменная SPACE_TERMINAL_DIR, другой порт — флаг --port или переменная SPACE_TERMINAL_PORT.

Отдельной команды удаления в st нет. Удалить свой плагин из терминала можно запросом к локальному API, например на macOS:

Окно терминала
curl -X DELETE \
-H "Authorization: Bearer $(cat "$HOME/Library/Application Support/Space Terminal/screeners/local_api_token")" \
http://127.0.0.1:5055/api/v1/screeners/you.my-screener

Проверять каждую правку в живом терминале долго. Запишите данные один раз и прогоняйте плагин на записи без терминала:

Окно терминала
st record --seconds 120 # плагин должен работать в открытой панели
st test # прогнать все записи и сравнить результат

st record перезапускает плагин в терминале и сохраняет всё, что терминал ему отвечал, в recordings/<время>.json; длительность — от 5 до 600 секунд. st test прогоняет каждую запись и сравнивает строки, всплывающие сообщения, статус и клики с файлом recordings/<запись>.expected.json — первый прогон его создаёт. После намеренной правки обновите эталон: st test --update. Тест падает и тогда, когда плагин вернул ошибку, упал или вышел за лимит процессора.

Для публикации запись должна быть не больше 4 МиБ — если больше, st record предупредит, сколько секунд записывать.

ЧтоЛимит
ПамятьДо 64 МБ
Процессор на вызовcpu_ms_per_call: по умолчанию 250 мс, от 50 до 1000. Ожидание сети не считается
Процессор в среднемНе больше 30 секунд за любые 60 секунд — половина ядра
Время одного вызоваДо 600 секунд вместе с ожиданием сети
Нарушения3 нарушения за последние 10 вызовов — плагин останавливается со статусом превышен лимит
HTTPТайм-аут от 1 до 10 секунд, ответ до 8 МиБ. К fapi.binance.com — не чаще 5 запросов в секунду
ТаблицаДо 10 000 строк, строка — до 16 КиБ
Сохранённые данные (kv)До 1 МиБ
Всплывающие сообщенияНе чаще 6 в минуту
Модуль screener.wasmДо 10 МиБ; функций — до 10 000, одна функция — до 256 КиБ кода

После сбоя терминал перезапускает плагин сам; память при этом теряется, kv сохраняется.

СообщениеЧто делать
cannot read the terminal tokenТерминал ещё ни разу не запускался на этом компьютере, или SPACE_TERMINAL_DIR указывает не туда
Space Terminal is not reachableТерминал не запущен, выключен Локальный API или сменён порт — укажите его флагом --port
unauthorized: the terminal rejected the tokenТокен взят из папки другого терминала — проверьте SPACE_TERMINAL_DIR
this Space Terminal build has no screener APIТерминал старше 0.104.71 — обновите его
forbidden_import: wasi_snapshot_preview1::…Код обращается к файлам, сети, потокам или системному времени напрямую. Собирайте через st build, данные берите у терминала
паника time not implementedСистемного времени в плагине нет — берите now_ms из on_timer или now_ms()
host_not_allowedАдреса нет в http манифеста — добавьте его точное имя
ответ 301 или 302 с пустым теломПереадресации не выполняются — обращайтесь сразу по конечному адресу
invalid_wasm: function N: … KiB of codeОдна функция слишком большая — обычно огромный match или таблица прямо в коде. Вынесите данные в static, разбейте функцию
статус превышен лимитРазбирайте из ответов бирж только нужные поля, отдавайте меньше строк, поднимите cpu_ms_per_call, реже вызывайте on_timer
not_in_clickОткрыть стакан из кода можно только внутри on_click
st test: N not recordedКод запрашивает данные, которых нет в записи, — запишите заново

Полная таблица — в разделе «Pitfalls» SKILL.md.