Нахожусь в процессе написания механизма торгового робота, работающего на Московской бирже через 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);
Итоги
Проект полностью представлен на Гитхабе. Новые модули будут загружаться по мере написания и тестирования.
Автор: Михаил Шардин
27 ноября 2024 г.