mirror of
https://github.com/ada-dmitry/dca_bot_tinv.git
synced 2026-09-24 08:40:19 +00:00
Init Repo
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
TINKOFF_TOKEN="your_tinvest_token" # Необходим токен с полным доступом.
|
||||||
|
TINKOFF_ACCOUNT_ID="your_account_id" # Опционально
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Dmitry
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
# DCA Bot для Tinkoff Invest
|
||||||
|
|
||||||
|
Автоматический бот для реализации стратегии Dollar Cost Averaging (DCA) через API Tinkoff Invest. Поддерживает работу с акциями, ETF, облигациями и валютами на реальном брокерском счете.
|
||||||
|
|
||||||
|
## Что это такое?
|
||||||
|
|
||||||
|
DCA (Dollar Cost Averaging) — это инвестиционная стратегия, при которой вы регулярно покупаете финансовые инструменты на фиксированную сумму, независимо от их текущей цены. Это помогает снизить влияние волатильности рынка на общую стоимость инвестиций.
|
||||||
|
|
||||||
|
## Основные возможности
|
||||||
|
|
||||||
|
- ✅ Автоматическое распределение бюджета по заданным весам между инструментами
|
||||||
|
- ✅ Поддержка акций, ETF, облигаций и валют
|
||||||
|
- ✅ Корректный расчет стоимости облигаций (номинал + НКД)
|
||||||
|
- ✅ Проверка торгового статуса инструментов
|
||||||
|
- ✅ Защита от ошибок недостатка средств с автоматическим уменьшением размера ордера
|
||||||
|
- ✅ Детальное логирование всех операций в CSV
|
||||||
|
- ✅ Режим симуляции (dry-run) для тестирования
|
||||||
|
- ✅ Буферы для комиссий и проскальзывания
|
||||||
|
- ✅ Работа только с реальными счетами (без песочницы)
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Реальный брокерский счет в Tinkoff
|
||||||
|
- API токен с правами на торговлю
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
1. Клонируйте репозиторий:
|
||||||
|
```bash
|
||||||
|
git clone <url>
|
||||||
|
cd dca_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Установите зависимости:
|
||||||
|
|
||||||
|
> Рекомендуется использование виртуального окружения Python.
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Скопируйте файл с переменными окружения:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Заполните `.env` файл:
|
||||||
|
```env
|
||||||
|
TINKOFF_TOKEN="ваш_токен"
|
||||||
|
TINKOFF_ACCOUNT_ID="ваш_id_счета" # опционально
|
||||||
|
```
|
||||||
|
|
||||||
|
## Настройка
|
||||||
|
|
||||||
|
Создайте файл конфигурации `config.yml` со списком инструментов и их весами:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
assets:
|
||||||
|
- figi: "BBG004730N88" # Сбербанк
|
||||||
|
weight: 0.3 # 30%
|
||||||
|
- figi: "BBG00475KKY8" # Яндекс
|
||||||
|
weight: 0.2 # 20%
|
||||||
|
- figi: "BBG333333333" # ОФЗ
|
||||||
|
weight: 0.3 # 30%
|
||||||
|
- figi: "BBG444444444" # Доллар США
|
||||||
|
weight: 0.2 # 20%
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно:** Сумма весов должна равняться 1.0!
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Основная команда
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py --config config.yml --rub-budget 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Режим симуляции (тестирование)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py --config config.yml --rub-budget 10000 --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Дополнительные параметры
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py \
|
||||||
|
--config config.yml \
|
||||||
|
--rub-budget 10000 \
|
||||||
|
--dry-run \
|
||||||
|
--fee-buf-bps 300 \
|
||||||
|
--safe-rub-pct 0.97 \
|
||||||
|
--wait-tradable-sec 60 \
|
||||||
|
--poll-sec 10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
- `--config` — путь к YAML файлу конфигурации (обязательно)
|
||||||
|
- `--rub-budget` — общий бюджет в рублях для данного запуска (обязательно)
|
||||||
|
- `--dry-run` — режим симуляции, реальные ордера не размещаются
|
||||||
|
- `--fee-buf-bps` — буфер для комиссий в базисных пунктах (по умолчанию 300 = 3%)
|
||||||
|
- `--safe-rub-pct` — использовать только N% от доступных средств (по умолчанию 0.97 = 97%)
|
||||||
|
- `--wait-tradable-sec` — ждать до N секунд пока инструмент станет торгуемым (по умолчанию 0)
|
||||||
|
- `--poll-sec` — интервал проверки торгового статуса в секундах (по умолчанию 10)
|
||||||
|
|
||||||
|
## Вспомогательные инструменты
|
||||||
|
|
||||||
|
В папке `tools/` находятся полезные утилиты для работы с API Tinkoff Invest:
|
||||||
|
|
||||||
|
### 1. Проверка баланса (`check_balance.py`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/check_balance.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Назначение:** Показывает текущее состояние вашего брокерского счета.
|
||||||
|
|
||||||
|
**Что показывает:**
|
||||||
|
- ID активного счета
|
||||||
|
- Свободные рубли (RUB free)
|
||||||
|
- Заблокированные рубли (RUB blocked)
|
||||||
|
- Доступные для торговли рубли (RUB available)
|
||||||
|
- Общая оценка портфеля в валютах (если доступно)
|
||||||
|
|
||||||
|
**Пример вывода:**
|
||||||
|
```
|
||||||
|
ACCOUNT_ID: 2235046505
|
||||||
|
RUB free: 25000.00
|
||||||
|
RUB blocked: 1500.00
|
||||||
|
RUB available: 23500.00
|
||||||
|
Portfolio total_amount_currencies: 150000.50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Когда использовать:**
|
||||||
|
- Перед запуском DCA бота для проверки достаточности средств
|
||||||
|
- Для мониторинга состояния счета
|
||||||
|
- При отладке проблем с размещением ордеров
|
||||||
|
|
||||||
|
### 2. Проверка инструментов (`check_figi.py`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/check_figi.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Назначение:** Проверяет корректность FIGI из вашей конфигурации и получает актуальные рыночные данные.
|
||||||
|
|
||||||
|
**Настройка:** Отредактируйте список `FIGI` в начале файла, добавив ваши FIGI из `config.yml`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
FIGI = [
|
||||||
|
"BBG004730N88", # Сбербанк
|
||||||
|
"BBG00475KKY8", # Яндекс
|
||||||
|
"BBG0013HGFT4", # USD000UTSTOM
|
||||||
|
# добавьте ваши FIGI здесь
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что показывает:**
|
||||||
|
- Информацию об инструменте (тикер, название, размер лота, валюта)
|
||||||
|
- Текущие рыночные цены
|
||||||
|
- Список инструментов без доступных цен
|
||||||
|
|
||||||
|
**Пример вывода:**
|
||||||
|
```
|
||||||
|
BBG004730N88: SBER | ПАО Сбербанк | lot=10 | curr=rub
|
||||||
|
BBG00475KKY8: YNDX | Яндекс Н.В. | lot=1 | curr=rub
|
||||||
|
PRICE BBG004730N88: 285.50
|
||||||
|
PRICE BBG00475KKY8: 2850.00
|
||||||
|
Нет last price для: []
|
||||||
|
```
|
||||||
|
|
||||||
|
**Когда использовать:**
|
||||||
|
- При настройке нового `config.yml`
|
||||||
|
- Для проверки актуальности FIGI перед запуском
|
||||||
|
- При отладке проблем с получением цен
|
||||||
|
|
||||||
|
### 3. Поиск инструментов (`search.py`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/search.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Назначение:** Помогает найти FIGI для нужных финансовых инструментов по тикеру или названию.
|
||||||
|
|
||||||
|
**Текущие поисковые запросы в скрипте:**
|
||||||
|
- Поиск золота по тикеру "TGLD@"
|
||||||
|
- Поиск ОФЗ 26212 по названию
|
||||||
|
- Поиск ETF "TPAY"
|
||||||
|
- Поиск основных валют (USD, EUR, GBP, JPY)
|
||||||
|
|
||||||
|
**Как настроить под свои нужды:**
|
||||||
|
|
||||||
|
1. **Поиск по тикеру акции:**
|
||||||
|
```python
|
||||||
|
shares = client.instruments.shares()
|
||||||
|
for s in shares.instruments:
|
||||||
|
if s.ticker == "SBER": # замените на нужный тикер
|
||||||
|
print(s.ticker, s.figi, s.name)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Поиск облигаций по названию:**
|
||||||
|
```python
|
||||||
|
bonds = client.instruments.bonds()
|
||||||
|
for b in bonds.instruments:
|
||||||
|
if "Сбербанк" in b.name: # замените на нужное название
|
||||||
|
print(b.name, b.figi)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Поиск ETF:**
|
||||||
|
```python
|
||||||
|
etfs = client.instruments.etfs()
|
||||||
|
for e in etfs.instruments:
|
||||||
|
if "TECH" in e.ticker: # замените на нужный тикер
|
||||||
|
print(e.ticker, e.figi, e.name)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Поиск валют:**
|
||||||
|
```python
|
||||||
|
currencies = client.instruments.currencies()
|
||||||
|
for cur in currencies.instruments:
|
||||||
|
if "USD" in (cur.ticker or ""):
|
||||||
|
print(cur.ticker, cur.figi, cur.name)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример вывода:**
|
||||||
|
```
|
||||||
|
TGLD@ BBG222222222 Золото
|
||||||
|
ОФЗ 26212 BBG00Y91R9T3 ОФЗ 26212
|
||||||
|
TPAY BBG111111111 TCS Payments
|
||||||
|
USD000UTSTOM BBG0013HGFT4 Доллар США
|
||||||
|
```
|
||||||
|
|
||||||
|
**Когда использовать:**
|
||||||
|
- При составлении нового портфеля
|
||||||
|
- Для поиска альтернативных инструментов
|
||||||
|
- При обновлении FIGI (например, для новых выпусков ОФЗ)
|
||||||
|
|
||||||
|
### Полезные советы по работе с инструментами
|
||||||
|
|
||||||
|
1. **Всегда проверяйте FIGI** перед добавлением в конфигурацию с помощью `check_figi.py`
|
||||||
|
|
||||||
|
2. **Для облигаций** FIGI может изменяться при новых выпусках, регулярно обновляйте
|
||||||
|
|
||||||
|
3. **Валютные пары** имеют разные FIGI для разных сроков поставки (TOM, TOD, SPOT)
|
||||||
|
|
||||||
|
4. **ETF и фонды** могут иметь ограничения по времени торговли
|
||||||
|
|
||||||
|
5. **Используйте поиск** для нахождения похожих инструментов в той же отрасли
|
||||||
|
|
||||||
|
### Автоматизация проверок
|
||||||
|
|
||||||
|
Можно создать скрипт для автоматической проверки перед запуском DCA:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
echo "=== Проверка баланса ==="
|
||||||
|
python tools/check_balance.py
|
||||||
|
|
||||||
|
echo -e "\n=== Проверка инструментов ==="
|
||||||
|
python tools/check_figi.py
|
||||||
|
|
||||||
|
echo -e "\n=== Запуск DCA (dry-run) ==="
|
||||||
|
python main.py --config config.yml --rub-budget 10000 --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Логирование
|
||||||
|
|
||||||
|
Все операции записываются в файл `orders_log.csv` со следующими полями:
|
||||||
|
|
||||||
|
- `ts` — временная метка
|
||||||
|
- `period_key` — ключ периода (YYYY-MM)
|
||||||
|
- `figi` — FIGI инструмента
|
||||||
|
- `ticker` — тикер
|
||||||
|
- `name` — название инструмента
|
||||||
|
- `instrument_type` — тип (share, bond, etf, currency)
|
||||||
|
- `lots` — количество лотов
|
||||||
|
- `lot_size` — размер лота
|
||||||
|
- `filled_price_per_share` — цена исполнения за бумагу
|
||||||
|
- `cost_rub` — общая стоимость в рублях
|
||||||
|
- `status` — статус операции
|
||||||
|
- `order_request_id` — ID запроса (если не dry-run)
|
||||||
|
|
||||||
|
## Особенности работы
|
||||||
|
|
||||||
|
### Облигации
|
||||||
|
Для облигаций автоматически рассчитывается полная стоимость с учетом номинала и накопленного купонного дохода (НКД).
|
||||||
|
|
||||||
|
### Защита от ошибок
|
||||||
|
- Автоматическое уменьшение размера ордера при ошибке 30042 (недостаток средств)
|
||||||
|
- Проверка торгового статуса инструментов
|
||||||
|
- Пропуск неторгуемых инструментов
|
||||||
|
- Буферы для комиссий и проскальзывания
|
||||||
|
|
||||||
|
### Валюты
|
||||||
|
Поддерживаются только инструменты в рублях. Валютные инструменты пропускаются.
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
1. **Никогда не коммитьте `.env` файл** — он содержит секретные токены
|
||||||
|
2. **Используйте токены только с необходимыми правами**
|
||||||
|
3. **Тестируйте конфигурацию в режиме `--dry-run`**
|
||||||
|
4. **Проверяйте баланс перед запуском**
|
||||||
|
|
||||||
|
## Получение токена
|
||||||
|
|
||||||
|
1. Перейдите в [личный кабинет Tinkoff Invest](https://www.tinkoff.ru/invest/)
|
||||||
|
2. Настройки → API → Создать токен
|
||||||
|
3. Выберите права: "Торговля" и "Чтение счетов"
|
||||||
|
4. Скопируйте токен в `.env` файл
|
||||||
|
|
||||||
|
## Автоматизация
|
||||||
|
|
||||||
|
Для регулярного запуска можно использовать cron:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Каждый понедельник в 10:00
|
||||||
|
0 10 * * 1 cd /path/to/dca_bot && python main.py --config config.yml --rub-budget 10000
|
||||||
|
|
||||||
|
# Каждое 1 число месяца в 15:30
|
||||||
|
30 15 1 * * cd /path/to/dca_bot && python main.py --config config.yml --rub-budget 50000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
dca_bot/
|
||||||
|
├── main.py # Основной скрипт
|
||||||
|
├── config.yml # Конфигурация портфеля (создать самостоятельно)
|
||||||
|
├── requirements.txt # Зависимости Python
|
||||||
|
├── .env # Переменные окружения (создать из .env.example)
|
||||||
|
├── .env.example # Шаблон переменных окружения
|
||||||
|
├── orders_log.csv # Лог операций (создается автоматически)
|
||||||
|
├── tools/
|
||||||
|
│ ├── check_balance.py # Проверка баланса
|
||||||
|
│ ├── check_figi.py # Проверка инструментов
|
||||||
|
│ └── search.py # Поиск инструментов
|
||||||
|
└── README.md # Этот файл
|
||||||
|
```
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## Отказ от ответственности
|
||||||
|
|
||||||
|
Этот бот предоставляется "как есть" без каких-либо гарантий. Автор не несет ответственности за возможные финансовые потери. Используйте на свой страх и риск. Всегда тестируйте в режиме `--dry-run` перед реальной торговлей.
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
assets:
|
||||||
|
# ОФЗ-ПД 26212 (погашение 2028 г.)
|
||||||
|
- name: "ОФЗ 26212"
|
||||||
|
figi: "BBG00425VG07"
|
||||||
|
weight: 0.40
|
||||||
|
|
||||||
|
# Биржевой фонд Т-Банка (TPAY)
|
||||||
|
- name: "TPAY"
|
||||||
|
figi: "TCS00A108WX3"
|
||||||
|
weight: 0.25
|
||||||
|
|
||||||
|
- name: "TMOS@"
|
||||||
|
figi: "TCS60A101X76"
|
||||||
|
weight: 0.25
|
||||||
|
|
||||||
|
- name: "TGLD@"
|
||||||
|
figi: "TCS80A101X50"
|
||||||
|
weight: 0.10
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import datetime as dt
|
||||||
|
from uuid import uuid4
|
||||||
|
from decimal import Decimal, ROUND_DOWN
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from tinkoff.invest import (
|
||||||
|
Client,
|
||||||
|
MoneyValue,
|
||||||
|
Quotation,
|
||||||
|
OrderDirection,
|
||||||
|
OrderType,
|
||||||
|
InstrumentIdType,
|
||||||
|
)
|
||||||
|
from tinkoff.invest.utils import quotation_to_decimal
|
||||||
|
from tinkoff.invest.exceptions import RequestError
|
||||||
|
|
||||||
|
|
||||||
|
CSV_LOG = "orders_log.csv"
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------- utils -----------------------------
|
||||||
|
def dec(v: Quotation | MoneyValue | None, default: str = "0") -> Decimal:
|
||||||
|
"""Безопасное преобразование Quotation/MoneyValue -> Decimal."""
|
||||||
|
if v is None:
|
||||||
|
return Decimal(default)
|
||||||
|
return quotation_to_decimal(v)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_csv(path: str):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
with open(path, "w", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(
|
||||||
|
[
|
||||||
|
"ts",
|
||||||
|
"period_key",
|
||||||
|
"figi",
|
||||||
|
"ticker",
|
||||||
|
"name",
|
||||||
|
"instrument_type",
|
||||||
|
"lots",
|
||||||
|
"lot_size",
|
||||||
|
"filled_price_per_share",
|
||||||
|
"cost_rub",
|
||||||
|
"status",
|
||||||
|
"order_request_id",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def append_log(**kwargs):
|
||||||
|
ensure_csv(CSV_LOG)
|
||||||
|
with open(CSV_LOG, "a", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(
|
||||||
|
[
|
||||||
|
dt.datetime.now(dt.timezone.utc).isoformat(),
|
||||||
|
kwargs.get("period_key", ""),
|
||||||
|
kwargs.get("figi", ""),
|
||||||
|
kwargs.get("ticker", ""),
|
||||||
|
kwargs.get("name", ""),
|
||||||
|
kwargs.get("instrument_type", ""),
|
||||||
|
kwargs.get("lots", 0),
|
||||||
|
kwargs.get("lot_size", 0),
|
||||||
|
str(kwargs.get("filled_price_per_share", "")),
|
||||||
|
str(kwargs.get("cost_rub", "")),
|
||||||
|
kwargs.get("status", ""),
|
||||||
|
kwargs.get("order_request_id", ""),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def insert_report_separator():
|
||||||
|
"""Добавляет пустую строку в CSV перед новой серией транзакций."""
|
||||||
|
ensure_csv(CSV_LOG)
|
||||||
|
with open(CSV_LOG, "a", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow([]) # просто пустая строка
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------- config -----------------------------
|
||||||
|
def load_config(path: str) -> dict:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
cfg = yaml.safe_load(f) or {}
|
||||||
|
assets = cfg.get("assets", [])
|
||||||
|
if not assets:
|
||||||
|
raise ValueError(
|
||||||
|
"Config must include non-empty 'assets' list under key 'assets'"
|
||||||
|
)
|
||||||
|
total_w = sum(float(a.get("weight", 0.0)) for a in assets)
|
||||||
|
if abs(total_w - 1.0) > 1e-6:
|
||||||
|
raise ValueError(f"Sum of weights must be 1.0; got {total_w}")
|
||||||
|
for a in assets:
|
||||||
|
if "figi" not in a or "weight" not in a:
|
||||||
|
raise ValueError("Each asset must have 'figi' and 'weight' fields")
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------- market helpers -----------------------------
|
||||||
|
def pick_account_id(client: Client, explicit: Optional[str]) -> str:
|
||||||
|
"""Реальный брокерский счёт (без песочницы)."""
|
||||||
|
if explicit:
|
||||||
|
return explicit
|
||||||
|
accs = list(client.users.get_accounts().accounts)
|
||||||
|
if not accs:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Нет реальных брокерских счетов. Проверь токен/права или создай счёт."
|
||||||
|
)
|
||||||
|
return accs[0].id
|
||||||
|
|
||||||
|
|
||||||
|
def get_instrument_meta(client: Client, figi: str) -> dict:
|
||||||
|
"""Мини‑карточка инструмента: лот, валюта, тип, UID, и (если доступны) nominal/aci для облигаций."""
|
||||||
|
resp = client.instruments.get_instrument_by(
|
||||||
|
id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI,
|
||||||
|
id=figi,
|
||||||
|
)
|
||||||
|
inst = resp.instrument
|
||||||
|
if inst is None:
|
||||||
|
raise ValueError(f"Instrument not found for FIGI {figi}")
|
||||||
|
|
||||||
|
# Пытаемся взять номинал/НКД (есть не во всех версиях/для всех инструментов)
|
||||||
|
nominal = dec(getattr(inst, "nominal", None), "0")
|
||||||
|
aci_value = dec(getattr(inst, "aci_value", None), "0")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": inst.name,
|
||||||
|
"ticker": inst.ticker,
|
||||||
|
"lot": inst.lot,
|
||||||
|
"currency": inst.currency, # 'rub'
|
||||||
|
"min_price_increment": dec(inst.min_price_increment, "0.01"),
|
||||||
|
"uid": inst.uid,
|
||||||
|
"type": str(
|
||||||
|
getattr(inst, "instrument_type", "")
|
||||||
|
).lower(), # 'bond' | 'share' | 'etf' ...
|
||||||
|
"nominal": nominal, # 0 если поле отсутствует
|
||||||
|
"aci_value": aci_value, # 0 если поле отсутствует
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_last_prices(client: Client, figis: List[str]) -> Dict[str, Decimal]:
|
||||||
|
prices: Dict[str, Decimal] = {}
|
||||||
|
resp = client.market_data.get_last_prices(figi=figis)
|
||||||
|
for p in resp.last_prices:
|
||||||
|
prices[p.figi] = dec(p.price)
|
||||||
|
missing = [f for f in figis if f not in prices]
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError(f"Missing last price for: {missing}")
|
||||||
|
return prices
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_rub(client: Client, account_id: str) -> Decimal:
|
||||||
|
"""Доступные RUB = свободные - заблокированные."""
|
||||||
|
pos = client.operations.get_positions(account_id=account_id)
|
||||||
|
rub_money = next(
|
||||||
|
(m for m in pos.money if m.currency.lower() == "rub"), None)
|
||||||
|
rub_blocked = next(
|
||||||
|
(m for m in pos.blocked if m.currency.lower() == "rub"), None)
|
||||||
|
free_rub = dec(rub_money) if rub_money else Decimal(0)
|
||||||
|
blocked_rub = dec(rub_blocked) if rub_blocked else Decimal(0)
|
||||||
|
return free_rub - blocked_rub
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------- sizing -----------------------------
|
||||||
|
def floor_lots(budget_rub: Decimal, eff_lot_cost: Decimal) -> int:
|
||||||
|
"""Сколько лотов помещается в бюджет."""
|
||||||
|
if eff_lot_cost <= 0:
|
||||||
|
return 0
|
||||||
|
lots = (budget_rub / eff_lot_cost).to_integral_value(rounding=ROUND_DOWN)
|
||||||
|
return int(max(lots, 0))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------- main -----------------------------
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="DCA bot for Tinkoff Invest (MOEX, real account only, .env-based)"
|
||||||
|
)
|
||||||
|
parser.add_argument("--config", required=True, help="Path to YAML config")
|
||||||
|
parser.add_argument(
|
||||||
|
"--rub-budget",
|
||||||
|
type=Decimal,
|
||||||
|
required=True,
|
||||||
|
help="Total RUB budget for this run, e.g. 10000",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run", action="store_true", help="Simulation only, no real orders"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--fee-buf-bps",
|
||||||
|
type=int,
|
||||||
|
default=300,
|
||||||
|
help="Buffer in bps for fees/slippage/rounding. Default 300 (3.00%)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--safe-rub-pct",
|
||||||
|
type=float,
|
||||||
|
default=0.97,
|
||||||
|
help="Use only N% of available RUB to avoid 30042. Default 0.97",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--wait-tradable-sec",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="Wait up to N seconds until instrument becomes tradable (0 = no wait)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--poll-sec",
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
help="Polling interval for trading status while waiting",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
token = os.environ.get("TINKOFF_TOKEN", "")
|
||||||
|
if not token:
|
||||||
|
print("ERROR: TINKOFF_TOKEN is not set in environment (.env)", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
explicit_account = os.environ.get("TINKOFF_ACCOUNT_ID") or None
|
||||||
|
|
||||||
|
cfg = load_config(args.config)
|
||||||
|
fee_buf = Decimal(args.fee_buf_bps) / Decimal(10000)
|
||||||
|
safe_mul = Decimal(str(args.safe_rub_pct))
|
||||||
|
|
||||||
|
with Client(token) as client:
|
||||||
|
account_id = pick_account_id(client, explicit_account)
|
||||||
|
|
||||||
|
figis = [a["figi"] for a in cfg["assets"]]
|
||||||
|
prices = fetch_last_prices(client, figis)
|
||||||
|
metas = {
|
||||||
|
a["figi"]: get_instrument_meta(client, a["figi"]) for a in cfg["assets"]
|
||||||
|
}
|
||||||
|
|
||||||
|
period_key = dt.datetime.now().strftime("%Y-%m")
|
||||||
|
|
||||||
|
insert_report_separator()
|
||||||
|
|
||||||
|
for asset in cfg["assets"]:
|
||||||
|
figi = asset["figi"]
|
||||||
|
weight = Decimal(str(asset["weight"]))
|
||||||
|
alloc = (args.rub_budget * weight).quantize(Decimal("0.01"))
|
||||||
|
meta = metas[figi]
|
||||||
|
price_last = prices[figi] # Quotation -> Decimal
|
||||||
|
lot = meta["lot"]
|
||||||
|
uid = meta["uid"]
|
||||||
|
inst_type = meta["type"] # 'bond'/'share'/'etf'/...
|
||||||
|
|
||||||
|
# Пропуск не-RUB
|
||||||
|
if str(meta["currency"]).lower() != "rub":
|
||||||
|
append_log(
|
||||||
|
period_key=period_key,
|
||||||
|
figi=figi,
|
||||||
|
ticker=meta["ticker"],
|
||||||
|
name=meta["name"],
|
||||||
|
instrument_type=inst_type,
|
||||||
|
lots=0,
|
||||||
|
lot_size=lot,
|
||||||
|
filled_price_per_share=price_last,
|
||||||
|
cost_rub=Decimal(0),
|
||||||
|
status=f"SKIPPED_NON_RUB_CURRENCY({meta['currency']})",
|
||||||
|
order_request_id="",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- Корректная цена за 1 бумагу ---
|
||||||
|
# Акции/ETF: last_price уже в ₽.
|
||||||
|
# Облигации: last_price — это % от номинала. Берём (price% * nominal) + НКД.
|
||||||
|
if inst_type == "bond":
|
||||||
|
nominal = meta.get("nominal") or Decimal("0")
|
||||||
|
if nominal <= 0:
|
||||||
|
nominal = Decimal("1000") # безопасный дефолт
|
||||||
|
aci = meta.get("aci_value") or Decimal("0")
|
||||||
|
clean_rub = (price_last / Decimal("100")) * nominal
|
||||||
|
price_per_share_effective = clean_rub + aci
|
||||||
|
else:
|
||||||
|
price_per_share_effective = price_last
|
||||||
|
|
||||||
|
# Полная стоимость 1 лота + буфер
|
||||||
|
eff_lot_cost = (
|
||||||
|
price_per_share_effective *
|
||||||
|
Decimal(lot) * (Decimal(1) + fee_buf)
|
||||||
|
).quantize(Decimal("0.01"))
|
||||||
|
|
||||||
|
# Первый расчёт лотов от аллокации
|
||||||
|
lots = floor_lots(alloc, eff_lot_cost)
|
||||||
|
if lots <= 0:
|
||||||
|
append_log(
|
||||||
|
period_key=period_key,
|
||||||
|
figi=figi,
|
||||||
|
ticker=meta["ticker"],
|
||||||
|
name=meta["name"],
|
||||||
|
instrument_type=inst_type,
|
||||||
|
lots=0,
|
||||||
|
lot_size=lot,
|
||||||
|
filled_price_per_share=price_per_share_effective,
|
||||||
|
cost_rub=Decimal(0),
|
||||||
|
status="SKIPPED_SMALL_ALLOC",
|
||||||
|
order_request_id="",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверим статус торгуемости; по желанию — подождём
|
||||||
|
def read_status():
|
||||||
|
st = client.market_data.get_trading_status(instrument_id=uid)
|
||||||
|
return (
|
||||||
|
bool(getattr(st, "api_trade_available_flag", False)),
|
||||||
|
bool(getattr(st, "market_order_available_flag", False)),
|
||||||
|
bool(getattr(st, "limit_order_available_flag", False)),
|
||||||
|
st.trading_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_ok, mkt_ok, lim_ok, tr_stat = read_status()
|
||||||
|
waited = 0
|
||||||
|
while (
|
||||||
|
args.wait_tradable_sec > 0
|
||||||
|
and api_ok
|
||||||
|
and (not mkt_ok and not lim_ok)
|
||||||
|
and waited < args.wait_tradable_sec
|
||||||
|
):
|
||||||
|
time.sleep(max(1, int(args.poll_sec)))
|
||||||
|
waited += max(1, int(args.poll_sec))
|
||||||
|
api_ok, mkt_ok, lim_ok, tr_stat = read_status()
|
||||||
|
|
||||||
|
if not api_ok or (not mkt_ok and not lim_ok):
|
||||||
|
append_log(
|
||||||
|
period_key=period_key,
|
||||||
|
figi=figi,
|
||||||
|
ticker=meta["ticker"],
|
||||||
|
name=meta["name"],
|
||||||
|
instrument_type=inst_type,
|
||||||
|
lots=0,
|
||||||
|
lot_size=lot,
|
||||||
|
filled_price_per_share=price_per_share_effective,
|
||||||
|
cost_rub=Decimal(0),
|
||||||
|
status=f"SKIPPED_TRADING_UNAVAILABLE(api={api_ok},mkt={mkt_ok},lim={
|
||||||
|
lim_ok},status={tr_stat},waited_s={waited})",
|
||||||
|
order_request_id="",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
order_type = (
|
||||||
|
OrderType.ORDER_TYPE_MARKET
|
||||||
|
if mkt_ok
|
||||||
|
else OrderType.ORDER_TYPE_BESTPRICE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Учтём реальный доступный кэш и добавим safety‑множитель
|
||||||
|
rub_available = get_available_rub(client, account_id)
|
||||||
|
safe_rub = (rub_available * safe_mul).quantize(Decimal("0.01"))
|
||||||
|
target_budget = min(alloc, safe_rub)
|
||||||
|
lots = min(lots, floor_lots(target_budget, eff_lot_cost))
|
||||||
|
|
||||||
|
if lots <= 0:
|
||||||
|
append_log(
|
||||||
|
period_key=period_key,
|
||||||
|
figi=figi,
|
||||||
|
ticker=meta["ticker"],
|
||||||
|
name=meta["name"],
|
||||||
|
instrument_type=inst_type,
|
||||||
|
lots=0,
|
||||||
|
lot_size=lot,
|
||||||
|
filled_price_per_share=price_per_share_effective,
|
||||||
|
cost_rub=Decimal(0),
|
||||||
|
status="SKIPPED_NOT_ENOUGH_RUB_AFTER_SAFE_CHECK",
|
||||||
|
order_request_id="",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
order_request_id = str(uuid4())
|
||||||
|
status = "DRY_RUN" if args.dry_run else "PENDING"
|
||||||
|
filled_price = Decimal(0)
|
||||||
|
cost_rub = Decimal(0)
|
||||||
|
|
||||||
|
if not args.dry_run:
|
||||||
|
# Авто‑даунсайз при 30042 (до 6 попыток)
|
||||||
|
attempts = 0
|
||||||
|
cur_lots = lots
|
||||||
|
while cur_lots > 0 and attempts < 6:
|
||||||
|
try:
|
||||||
|
resp = client.orders.post_order(
|
||||||
|
instrument_id=uid, # используем UID
|
||||||
|
quantity=cur_lots, # КОЛ-ВО ЛОТОВ
|
||||||
|
account_id=account_id,
|
||||||
|
direction=OrderDirection.ORDER_DIRECTION_BUY,
|
||||||
|
order_type=order_type,
|
||||||
|
order_id=str(uuid4()), # новый UUID на попытку
|
||||||
|
)
|
||||||
|
filled_price = dec(resp.executed_order_price)
|
||||||
|
cost_rub = dec(resp.total_order_amount)
|
||||||
|
status = str(resp.execution_report_status).replace(
|
||||||
|
"EXECUTION_REPORT_STATUS_", ""
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except RequestError as e:
|
||||||
|
if e.details == "30042": # Not enough assets for a margin trade
|
||||||
|
cur_lots -= 1
|
||||||
|
attempts += 1
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
e.details == "30079"
|
||||||
|
): # Instrument is not available for trading
|
||||||
|
status = f"SKIPPED_TRADING_UNAVAILABLE(api={api_ok},mkt={mkt_ok},lim={
|
||||||
|
lim_ok},status={tr_stat})"
|
||||||
|
cur_lots = 0
|
||||||
|
break
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
status = f"ERROR:{e!r}"
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
status = f"ERROR:{e!r}"
|
||||||
|
break
|
||||||
|
|
||||||
|
if status.startswith("PENDING") and cur_lots != lots:
|
||||||
|
status = f"DOWNSIZED_FROM_{lots}_TO_{cur_lots}"
|
||||||
|
if (
|
||||||
|
(cur_lots <= 0)
|
||||||
|
and not status.startswith("ERROR")
|
||||||
|
and "SKIPPED_TRADING_UNAVAILABLE" not in status
|
||||||
|
):
|
||||||
|
status = "SKIPPED_NOT_ENOUGH_RUB_RUNTIME"
|
||||||
|
|
||||||
|
lots_to_log = cur_lots
|
||||||
|
else:
|
||||||
|
lots_to_log = lots
|
||||||
|
|
||||||
|
append_log(
|
||||||
|
period_key=period_key,
|
||||||
|
figi=figi,
|
||||||
|
ticker=meta["ticker"],
|
||||||
|
name=meta["name"],
|
||||||
|
instrument_type=inst_type,
|
||||||
|
lots=lots_to_log,
|
||||||
|
lot_size=lot,
|
||||||
|
filled_price_per_share=filled_price or price_per_share_effective,
|
||||||
|
cost_rub=cost_rub,
|
||||||
|
status=status,
|
||||||
|
order_request_id=order_request_id if not args.dry_run else "",
|
||||||
|
)
|
||||||
|
|
||||||
|
time.sleep(0.2) # бережно к rate limit
|
||||||
|
|
||||||
|
print("Done. See orders_log.csv")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
PyYAML
|
||||||
|
tinkoff_invest
|
||||||
|
dotenv
|
||||||
|
tinkoff
|
||||||
|
tinkoff-invest
|
||||||
|
tinkoff-investments
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# check_balance.py
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from tinkoff.invest import Client
|
||||||
|
from tinkoff.invest.utils import quotation_to_decimal
|
||||||
|
from decimal import Decimal
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def dec_money(mv) -> Decimal:
|
||||||
|
# Работает и для MoneyValue, и для Quotation
|
||||||
|
return quotation_to_decimal(mv) if mv is not None else Decimal(0)
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
token = os.environ.get("TINKOFF_TOKEN")
|
||||||
|
account_id = os.environ.get("TINKOFF_ACCOUNT_ID")
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
print("ERROR: TINKOFF_TOKEN is not set")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
with Client(token) as client:
|
||||||
|
# Если account_id не задан, берём первый реальный счёт
|
||||||
|
if not account_id:
|
||||||
|
accs = client.users.get_accounts().accounts
|
||||||
|
if not accs:
|
||||||
|
print(
|
||||||
|
"Нет реальных счетов. Проверь права токена: accounts:read и наличие счёта.")
|
||||||
|
sys.exit(3)
|
||||||
|
account_id = accs[0].id
|
||||||
|
|
||||||
|
# Универсальный способ: get_positions -> money (остатки) и blocked (заморожено)
|
||||||
|
pos = client.operations.get_positions(account_id=account_id)
|
||||||
|
|
||||||
|
rub_money = next(
|
||||||
|
(m for m in pos.money if m.currency.lower() == "rub"), None)
|
||||||
|
rub_blocked = next(
|
||||||
|
(m for m in pos.blocked if m.currency.lower() == "rub"), None)
|
||||||
|
|
||||||
|
rub_free = dec_money(rub_money) if rub_money else Decimal(0)
|
||||||
|
rub_frozen = dec_money(rub_blocked) if rub_blocked else Decimal(0)
|
||||||
|
rub_available = rub_free - rub_frozen
|
||||||
|
|
||||||
|
print("ACCOUNT_ID:", account_id)
|
||||||
|
print("RUB free: ", rub_free)
|
||||||
|
print("RUB blocked: ", rub_frozen)
|
||||||
|
print("RUB available:", rub_available)
|
||||||
|
|
||||||
|
# Дополнительно: оценка портфеля (если нужно)
|
||||||
|
pf = client.operations.get_portfolio(account_id=account_id)
|
||||||
|
total_currencies = dec_money(pf.total_amount_currencies) if hasattr(
|
||||||
|
pf, "total_amount_currencies") else None
|
||||||
|
if total_currencies is not None:
|
||||||
|
print("Portfolio total_amount_currencies:", total_currencies)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# check_figi.py
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from tinkoff.invest import Client, InstrumentIdType
|
||||||
|
from tinkoff.invest.utils import quotation_to_decimal
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
FIGI = ["BBG00425VG07", "BBG0073DLHS1"
|
||||||
|
# вставь сюда твои FIGI из config.yaml
|
||||||
|
# "BBG004730N88", ...
|
||||||
|
]
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
token = os.environ.get("TINKOFF_TOKEN")
|
||||||
|
if not token:
|
||||||
|
print("ERROR: TINKOFF_TOKEN is not set", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
with Client(token) as client:
|
||||||
|
# Проверка карточек
|
||||||
|
for f in FIGI:
|
||||||
|
inst = client.instruments.get_instrument_by(
|
||||||
|
id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id=f
|
||||||
|
).instrument
|
||||||
|
if not inst:
|
||||||
|
print(f"{f}: NOT FOUND")
|
||||||
|
continue
|
||||||
|
print(
|
||||||
|
f"{f}: {inst.ticker} | {inst.name} | lot={
|
||||||
|
inst.lot} | curr={inst.currency}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверка last price
|
||||||
|
lp = client.market_data.get_last_prices(figi=FIGI)
|
||||||
|
seen = set()
|
||||||
|
for p in lp.last_prices:
|
||||||
|
price = quotation_to_decimal(p.price)
|
||||||
|
print(f"PRICE {p.figi}: {price}")
|
||||||
|
seen.add(p.figi)
|
||||||
|
missing = [f for f in FIGI if f not in seen]
|
||||||
|
if missing:
|
||||||
|
print("Нет last price для:", missing)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from tinkoff.invest import Client
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
TOKEN = os.getenv("TINKOFF_TOKEN")
|
||||||
|
|
||||||
|
|
||||||
|
with Client(TOKEN) as client:
|
||||||
|
# Все акции
|
||||||
|
shares = client.instruments.shares()
|
||||||
|
for s in shares.instruments:
|
||||||
|
if s.ticker == "TGLD@": # ищем по тикеру
|
||||||
|
print(s.ticker, s.figi, s.name)
|
||||||
|
|
||||||
|
# Все облигации
|
||||||
|
bonds = client.instruments.bonds()
|
||||||
|
for b in bonds.instruments:
|
||||||
|
if "ОФЗ 26212" in b.name: # ищем все ОФЗ по названию
|
||||||
|
print(b.name, b.figi)
|
||||||
|
|
||||||
|
# Все ETF
|
||||||
|
etfs = client.instruments.etfs()
|
||||||
|
for e in etfs.instruments:
|
||||||
|
if "TPAY" in e.ticker:
|
||||||
|
print(e.ticker, e.figi, e.name)
|
||||||
|
|
||||||
|
# Поиск валют по тикеру/названию и вывод figi
|
||||||
|
wanted = ["USD", "EUR", "GBP", "JPY"]
|
||||||
|
currencies = client.instruments.currencies()
|
||||||
|
for cur in currencies.instruments:
|
||||||
|
for w in wanted:
|
||||||
|
if w in (cur.ticker or "") or w in (cur.name or ""):
|
||||||
|
print(cur.ticker, cur.figi, cur.name)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Поиск золота по тикеру (ищем среди валют, фьючерсов, ETF и акций)
|
||||||
|
gold_tickers = ["TGLD@"]
|
||||||
|
lists_to_search = [
|
||||||
|
client.instruments.currencies(),
|
||||||
|
client.instruments.futures(),
|
||||||
|
client.instruments.etfs(),
|
||||||
|
client.instruments.shares(),
|
||||||
|
]
|
||||||
|
for lst in lists_to_search:
|
||||||
|
for inst in lst.instruments:
|
||||||
|
for gt in gold_tickers:
|
||||||
|
if gt in (inst.ticker or "") or gt in (inst.name or ""):
|
||||||
|
print(inst.ticker, inst.figi, inst.name)
|
||||||
|
break
|
||||||
Reference in New Issue
Block a user