Отслеживание позиций торгового робота Московской биржи через CSV файл
- четверг, 28 ноября 2024 г. в 00:00:06
Нахожусь в процессе написания механизма торгового робота, работающего на Московской бирже через API одного из брокеров. Брокеров имеющих своё АПИ для МосБиржи катастрофически мало — мне известно только о трёх. При этом, когда я стал публиковать модули робота (и полностью выложу готовый механизм робота на GitHub), то стал получать непонимание — например, мне писали в комментариях — зачем придумывать велосипед, когда уже есть QUIK — популярная российская платформа для биржевых торгов. В Квике уже есть готовый функционал «импорт транзакций из файла» или таблица «карман транзакций». В тех же комментариях предлагали даже рассмотреть использование платформы 1С для робота, но оказалось, что торговля все равно будет осуществляться через импорт .tri-файла
в Квик.
Лично мне Квик не очень нравится тем, что это программа для Windows. Хочется иметь механизм торгового робота, который был бы кроссплатформенным и легким — это позволит использовать его даже на «слабом» сервере. К тому же, много лет назад, когда Квик был единственной альтернативой для частного лица, невозможно было внутри одной Windows без использования виртуальной машины запустить несколько копий программы технического анализа с разными системами - для того, чтобы каждая из этих копий отправляла свои сигналы на покупку и продажу в соответствующий Квик. Это было нужно для разных торговых стратегий.
По субъективным причинам я стал писать торгового робота в среде исполнения JavaScript Node.js, но для тестирования на истории пришлось использовать Python и его библиотеки.
Вообще именно этот модуль пришлось пару раз переписывать, потому что не смог сразу отладить его. Проблема была в том, что вызов модуля записи и обновления позиций осуществлялся сразу из нескольких мест и одни результаты перезаписывали другие. Но удалось разобраться и теперь всё протестировано и работает.
Дополнительно использую библиотеки csv-parser
и json2csv
— это популярные инструменты Node.js для обработки данных CSV, каждая из которых служит различным целям:
csv-parser, это легкая и быстрая библиотека для анализа файлов CSV. Она основана на потоках, что делает ее очень эффективной для обработки больших наборов данных.
json2csv, это утилита для преобразования данных JSON в формат CSV. Идеально подходит для экспорта данных из приложений в структуру, удобную для CSV, может работать как синхронно, так и асинхронно.
Установка этих библиотек:
npm install csv-parser json2csv
Этот код определяет модуль для взаимодействия с 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
для использования в других частях робота.
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 };
Этот модуль важен для обеспечения согласованности данных между локальным CSV-файлом и текущими позициями, полученными из T‑Bank Invest API. Он проверяет наличие несоответствий, которые могут привести к ошибкам в торговых операциях, и останавливает робота, если обнаруживаются несоответствия.
Интеграция с внешними системами
T‑Bank Invest API: взаимодействует с API для извлечения торговых позиций в реальном времени.
CSV File Management: использует локальный CSV-файл для хранения и управления представлением бота о торговых позициях.
Проверка согласованности
Сравнивает позиции из CSV-файла с позициями с сервера T‑Bank Invest API.
Проверяет как количество, так и наличие позиций для обнаружения несоответствий.
Обработка ошибок
Регистрирует подробные ошибки при обнаружении несоответствий.
Останавливает торговые операции для предотвращения дальнейших действий на основе неверных данных.
Извлекает все открытые позиции из T‑Bank Invest API.
Извлекает позиции с ценными бумагами и преобразует баланс в float для сравнения.
Регистрирует ответ сервера для отладки и аудита.
Загружает данные CSV: считывает локальную запись позиций бота с помощью csvHandler
.
Сравнивает позиции:
Для каждой позиции CSV ищет соответствующую позицию на сервере с помощью FIGI (уникальный идентификатор).
Извлекает размер лота для точного сравнения количества.
Если обнаружены расхождения в количестве или отсутствующие позиции, регистрирует ошибки и останавливает торговлю.
Статус журнала: подтверждает, когда все позиции совпадают, и позволяет продолжить торговлю.
Извлечение позиций:
Локальные позиции загружаются из CSV-файла.
Позиции сервера извлекаются через API Tinkoff.
Обнаружение расхождений:
Для каждой позиции в CSV-файле:
Код вычисляет общее количество в лотах (csvPosition.quantity * lotSize
).
Сравнивает с балансом на сервере.
Ошибки регистрируются, если:
Количества не совпадают.
Позиция в CSV-файле отсутствует на сервере.
Безопасность робота:
Любые обнаруженные расхождения вызывают ошибку, останавливающую торговые операции.
Не позволяет роботу совершать сделки на основе устаревших или неверных данных.
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 г.