Нахожусь в процессе написания механизма торгового робота, работающего на Московской бирже через API одного из брокеров. Брокеров имеющих своё АПИ для МосБиржи катастрофически мало — мне известно только о трёх. При этом, когда я стал публиковать модули робота (и полностью выложу готовый механизм робота на GitHub), то стал получать непонимание — например, мне писали в комментариях — зачем придумывать велосипед, когда уже есть QUIK — популярная российская платформа для биржевых торгов. В Квике уже есть готовый функционал «импорт транзакций из файла» или таблица «карман транзакций». В тех же комментариях предлагали даже рассмотреть использование платформы 1С для робота, но оказалось, что торговля все равно будет осуществляться через импорт .tri-файла
в Квик.
Лично мне Квик не очень нравится тем, что это программа для Windows. Хочется иметь механизм торгового робота, который был бы кроссплатформенным и легким — это позволит использовать его даже на «слабом» сервере. К тому же, много лет назад, когда Квик был единственной альтернативой для частного лица, невозможно было внутри одной Windows без использования виртуальной машины запустить несколько копий программы технического анализа с разными системами — для того, чтобы каждая из этих копий отправляла свои сигналы на покупку и продажу в соответствующий Квик. Это было нужно для разных торговых стратегий.
По субъективным причинам я стал писать торгового робота в среде исполнения JavaScript Node.js, но для тестирования на истории пришлось использовать Python и его библиотеки.
Проблемы с записью позиций в Node.js
Вообще именно этот модуль пришлось пару раз переписывать, потому что не смог сразу отладить его. Проблема была в том, что вызов модуля записи и обновления позиций осуществлялся сразу из нескольких мест и одни результаты перезаписывали другие. Но удалось разобраться и теперь всё протестировано и работает.
Дополнительно использую библиотеки csv-parser
и json2csv
— это популярные инструменты Node.js для обработки данных CSV, каждая из которых служит различным целям:
-
csv-parser, это легкая и быстрая библиотека для анализа файлов CSV. Она основана на потоках, что делает ее очень эффективной для обработки больших наборов данных.
-
json2csv, это утилита для преобразования данных JSON в формат CSV. Идеально подходит для экспорта данных из приложений в структуру, удобную для CSV, может работать как синхронно, так и асинхронно.
Установка этих библиотек:
npm install csv-parser json2csv
Мой модуль csvHandler.js
Этот код определяет модуль для взаимодействия с CSV-файлом для управления финансовыми торговыми позициями. Служит для загрузки, сохранения, обновления и удаления финансовых позиций, хранящихся в CSV-файле.
Ключевые библиотеки:
-
fs
: для операций файловой системы, таких как чтение и запись файлов. -
csv-parser
: для анализа CSV-файлов в объекты JavaScript. -
json2csv
: для преобразования объектов JavaScript в формат CSV для сохранения. -
path
: для управления путями к файлам. -
Интеграция: включает пользовательские модули для ведения журнала (
logService
) и получения имен функций для лучшей отладки.
Функциональность
-
Обработка пути к файлу: использует модуль
path
для поиска CSV-файла, хранящего данные о позиции:../../data/+positions.csv
.
-
Функции управления позицией:
loadPositions():
-
Считывает CSV-файл и анализирует его в массив объектов позиции.
-
Преобразует числовые поля (quantity, purchasePrice, maxPrice, profitLoss) в числа с плавающей точкой для вычислений.
-
Возвращает обещание, которое разрешается с проанализированными данными или отклоняется в случае ошибки.
savePositions(positions):
-
Преобразует массив объектов позиции обратно в формат CSV с помощью json2csv.
-
Перезаписывает CSV-файл обновленными данными.
removePosition(figi):
-
Удаляет позицию из CSV-файла на основе ее figi (уникального идентификатора).
-
Загружает все позиции, отфильтровывает указанную и перезаписывает файл.
updatePosition(newPosition):
-
Добавляет новую позицию или обновляет существующую в CSV-файле:
-
Если figi существует, обновляет соответствующую позицию.
-
В противном случае добавляет новую позицию.
-
Сохраняет обновленный список обратно в CSV-файл.
-
Экспортированные модули: функции
loadPositions
,updatePosition
иremovePosition
для использования в других частях робота.
Полный код csvHandler.js:
const fs = require('fs'); const csv = require('csv-parser'); const { parse } = require('json2csv'); const path = require('path'); // Модуль для работы с путями файлов и директорий const filePath = path.join(__dirname, '../../data/+positions.csv'); // Путь к файлу CSV const logger = require('./logService'); // Подключаем модуль для логирования const logFunctionName = require('./logFunctionName'); // Модуль для получения имени функции (для логирования) // Загружаем все позиции из CSV файла function loadPositions() { return new Promise((resolve, reject) => { const positions = []; fs.createReadStream(filePath) .pipe(csv()) .on('data', (row) => { positions.push({ ticker: row.ticker, figi: row.figi, quantity: parseFloat(row.quantity), // Преобразование количества в float purchaseDate: row.purchaseDate, purchasePrice: parseFloat(row.purchasePrice), // Преобразование цены покупки в float updateDate: row.updateDate, maxPrice: parseFloat(row.maxPrice), // Преобразование максимальной цены в float profitLoss: parseFloat(row.profitLoss) // Преобразование прибыли/убытков в float }); }) .on('end', () => resolve(positions)) .on('error', reject); }); } // Сохраняем актуальные данные о позициях в CSV файл function savePositions(positions) { const csvFields = ['ticker', 'figi', 'quantity', 'purchaseDate', 'purchasePrice', 'updateDate', 'maxPrice', 'profitLoss']; const csvData = parse(positions, { fields: csvFields }); fs.writeFileSync(filePath, csvData); } // Удаляем позицию из CSV файла (после продажи) function removePosition(figi) { loadPositions().then(positions => { const updatedPositions = positions.filter(position => position.figi !== figi); savePositions(updatedPositions); }); } // Добавляем новую позицию или обновляем существующую в CSV файле function updatePosition(newPosition) { loadPositions().then(positions => { const index = positions.findIndex(pos => pos.figi === newPosition.figi); if (index === -1) { // Добавляем, если не нашли существующую позицию positions.push(newPosition); } else { // Обновляем, если позиция уже существует positions[index] = newPosition; } savePositions(positions); }); } module.exports = { loadPositions, updatePosition, removePosition };
Мой модуль checkCSVpositions.js
Этот модуль важен для обеспечения согласованности данных между локальным CSV-файлом и текущими позициями, полученными из T‑Bank Invest API. Он проверяет наличие несоответствий, которые могут привести к ошибкам в торговых операциях, и останавливает робота, если обнаруживаются несоответствия.
Основные функции
-
Интеграция с внешними системами
-
T‑Bank Invest API: взаимодействует с API для извлечения торговых позиций в реальном времени.
-
CSV File Management: использует локальный CSV-файл для хранения и управления представлением бота о торговых позициях.
-
Проверка согласованности
-
Сравнивает позиции из CSV-файла с позициями с сервера T‑Bank Invest API.
-
Проверяет как количество, так и наличие позиций для обнаружения несоответствий.
-
Обработка ошибок
-
Регистрирует подробные ошибки при обнаружении несоответствий.
-
Останавливает торговые операции для предотвращения дальнейших действий на основе неверных данных.
Основные функции:
1. getServerPositions()
-
Извлекает все открытые позиции из T‑Bank Invest API.
-
Извлекает позиции с ценными бумагами и преобразует баланс в float для сравнения.
-
Регистрирует ответ сервера для отладки и аудита.
2. checkForDiscrepancies()
-
Загружает данные CSV: считывает локальную запись позиций бота с помощью
csvHandler
.Сравнивает позиции:
-
Для каждой позиции CSV ищет соответствующую позицию на сервере с помощью FIGI (уникальный идентификатор).
-
Извлекает размер лота для точного сравнения количества.
-
Если обнаружены расхождения в количестве или отсутствующие позиции, регистрирует ошибки и останавливает торговлю.
-
Статус журнала: подтверждает, когда все позиции совпадают, и позволяет продолжить торговлю.
Рабочий процесс
-
Извлечение позиций:
-
Локальные позиции загружаются из CSV-файла.
-
Позиции сервера извлекаются через API Tinkoff.
-
Обнаружение расхождений:
Для каждой позиции в CSV-файле:
-
Код вычисляет общее количество в лотах (
csvPosition.quantity * lotSize
). -
Сравнивает с балансом на сервере.
Ошибки регистрируются, если:
-
Количества не совпадают.
-
Позиция в CSV-файле отсутствует на сервере.
-
Безопасность робота:
-
Любые обнаруженные расхождения вызывают ошибку, останавливающую торговые операции.
-
Не позволяет роботу совершать сделки на основе устаревших или неверных данных.
Полный код checkCSVpositions.js:
const logger = require('./logService'); // Логирование в файл и консоль const logFunctionName = require('./logFunctionName'); // Получение имени функции const secrets = require('../../config/secrets'); // Ключи доступа и идентификаторы const config = require('../../config/config'); // Параметры const csvHandler = require('./csvHandler'); // Работа с CSV файлами const TinkoffClient = require('../grpc/tinkoffClient'); // Модуль для взаимодействия с API Tinkoff Invest const API_TOKEN = secrets.TbankSandboxMode; const tinkoffClient = new TinkoffClient(API_TOKEN); // Функция для получения всех позиций с сервера async function getServerPositions() { try { const accountId = { accountId: secrets.AccountID }; const response = await tinkoffClient.callApi('OperationsService/GetPositions', accountId); // Логируем полученные позиции с сервера logger.info(`Все открытые позиции счета ${secrets.AccountID}:\n ${JSON.stringify(response, null, '\t')}\n\n`); // Возвращаем только позиции с ценными бумагами (securities) return response.securities.map(sec => ({ figi: sec.figi, balance: parseFloat(sec.balance) // Преобразуем баланс в float })); } catch (error) { logger.error(`Ошибка при получении позиций с сервера: ${error.message}`); throw error; } } // Функция для проверки расхождений async function checkForDiscrepancies() { try { // Загружаем текущие позиции из CSV файла var csvPositions = await csvHandler.loadPositions(); // Получаем позиции с сервера const serverPositions = await getServerPositions(); // Проверяем каждую позицию из CSV for (const csvPosition of csvPositions) { // Находим соответствующую позицию с сервера const serverPosition = serverPositions.find(pos => pos.figi === csvPosition.figi); if (serverPosition) { const lotSize = await tinkoffClient.getLot(csvPosition.figi); logger.info(`Количество бумаг в лоте ${csvPosition.figi}: ${lotSize} шт.`); const csvTotal = csvPosition.quantity * lotSize; // Сравниваем количество позиций if (csvTotal !== serverPosition.balance) { // Если есть расхождение, логируем ошибку и останавливаем торгового робота logger.error(`Ошибка: Несоответствие по FIGI ${csvPosition.figi}. CSV: ${csvTotal}, Сервер: ${serverPosition.balance}`); throw new Error('Найдено несоответствие позиций. Остановка торговли.'); } } else { logger.error(`Ошибка: Позиция с FIGI ${csvPosition.figi} отсутствует на сервере.`); throw new Error('Найдено несоответствие позиций. Остановка торговли.'); } } logger.info('Все позиции совпадают. Торговля продолжается.'); } catch (error) { logger.error(`Ошибка при проверке позиций: ${error.message}`); // Останавливаем торгового робота (добавьте здесь вашу логику остановки) } } // Экспортируем функции module.exports = { checkForDiscrepancies }; // checkForDiscrepancies().catch(logger.error);
Итоги
Проект полностью представлен на Гитхабе: https://github.com/empenoso/SilverFir-TradingBot.
Новые модули будут загружаться по мере написания и тестирования.
Автор: Михаил Шардин
27 ноября 2024 г.
ссылка на оригинал статьи https://habr.com/ru/articles/860162/
Добавить комментарий