Init Repo

This commit is contained in:
ada
2025-09-06 15:08:20 +03:00
commit c514b65862
9 changed files with 1002 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
TINKOFF_TOKEN="your_tinvest_token" # Необходим токен с полным доступом.
TINKOFF_ACCOUNT_ID="your_account_id" # Опционально
+21
View File
@@ -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.
+347
View File
@@ -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
View File
@@ -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
+461
View File
@@ -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()
+6
View File
@@ -0,0 +1,6 @@
PyYAML
tinkoff_invest
dotenv
tinkoff
tinkoff-invest
tinkoff-investments
+55
View File
@@ -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)
+42
View File
@@ -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)
+50
View File
@@ -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