├── tests
├── __init__.py
├── caches
│ ├── __init__.py
│ └── test_ttl_cache.py
├── marketdata
│ ├── __init__.py
│ └── test_async_marketdata.py
├── data_loaders
│ └── __init__.py
├── test_strategies
│ ├── __init__.py
│ └── test_moving_average
│ │ └── __init__.py
├── test_orders_canceling
│ ├── __init__.py
│ ├── test_orders_canceler.py
│ └── test_async_orders_canceler.py
├── utils.py
├── test_datetime_utils.py
├── test_signals.py
├── test_operations.py
├── test_stoporders.py
├── test_users.py
├── test_orders.py
├── test_protobuf_to_dataclass.py
├── test_utils.py
└── test_marketdata.py
├── examples
├── __init__.py
├── README.md
├── client.py
├── get_trading_statuses.py
├── sandbox_client.py
├── users
│ ├── get_user_info.py
│ ├── get_bank_accounts.py
│ ├── async_get_bank_accounts.py
│ ├── currency_transfer.py
│ └── async_currency_transfer.py
├── async_client.py
├── instruments
│ ├── options.py
│ ├── instruments.py
│ ├── get_brands.py
│ ├── instrument_find_by_ticker.py
│ ├── indicatives.py
│ ├── get_bonds.py
│ ├── get_asset_fundamentals.py
│ ├── async_get_bonds.py
│ ├── structured_notes.py
│ ├── structured_notes_by.py
│ ├── async_structured_notes_by.py
│ ├── async_structured_notes.py
│ ├── async_get_consensus_forecasts.py
│ ├── get_bond_events.py
│ ├── async_get_forecast_by.py
│ ├── async_get_assets.py
│ ├── get_consensus_forecasts.py
│ ├── get_asset_reports.py
│ ├── get_assets.py
│ ├── async_get_bond_events.py
│ ├── get_forecast_by.py
│ ├── async_get_asset_reports.py
│ ├── get_insider_deals.py
│ └── instrument_favorites.py
├── open_sandbox_account.py
├── async_get_trading_statuses.py
├── get_last_prices.py
├── get_strategies.py
├── porfolio_stream_client.py
├── get_last_trades.py
├── order_state_stream.py
├── async_get_strategies.py
├── async_indicatives.py
├── async_order_state_stream.py
├── async_get_last_prices.py
├── positions_stream.py
├── get_candles_with_limit.py
├── get_sandbox_max_lots.py
├── get_risk_rates.py
├── async_get_candles_with_limit.py
├── async_get_risk_rates.py
├── get_signals.py
├── cancel_orders.py
├── all_candles.py
├── get_active_orders.py
├── async_get_sandbox_max_lots.py
├── logger.py
├── async_all_candles.py
├── get_market_values.py
├── retrying_client.py
├── async_get_signals.py
├── post_order_async.py
├── get_operations_by_cursor.py
├── async_post_order_async.py
├── max_lots.py
├── async_get_market_values.py
├── async_retrying_client.py
├── post_order.py
├── easy_stream_client.py
├── download_all_candles.py
├── sandbox
│ ├── sandbox_get_stop_orders.py
│ ├── sandbox_cancel_stop_order.py
│ └── sandbox_post_stop_order.py
├── async_get_insider_deals.py
├── async_stream_client.py
├── get_orders.py
├── async_get_orders.py
├── stream_client.py
├── get_tech_analysis.py
├── instrument_cache.py
├── order_price.py
├── async_get_tech_analysis.py
├── wiseplat_cancel_all_stop_orders.py
├── easy_async_stream_client.py
├── async_instrument_favorites.py
├── trailing_stop.py
├── wiseplat_get_figi_for_ticker.py
├── strategies
│ └── moving_average.py
├── wiseplat_set_get_sandbox_balance.py
└── wiseplat_create_take_profit_stop_order.py
├── scripts
├── __init__.py
├── update_package_version.py
├── update_issue_templates.py
├── version.py
└── download_protos.py
├── tinkoff
├── __init__.py
└── invest
│ ├── py.typed
│ ├── grpc
│ ├── __init__.py
│ ├── common_pb2_grpc.py
│ └── google
│ │ └── api
│ │ ├── field_behavior_pb2_grpc.py
│ │ └── field_behavior_pb2.py
│ ├── caching
│ ├── __init__.py
│ ├── instruments_cache
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ ├── models.py
│ │ ├── protocol.py
│ │ ├── interface.py
│ │ └── instrument_storage.py
│ ├── market_data_cache
│ │ ├── __init__.py
│ │ ├── datetime_range.py
│ │ ├── serialization.py
│ │ ├── instrument_date_range_market_data.py
│ │ ├── interface.py
│ │ └── cache_settings.py
│ └── overrides.py
│ ├── retrying
│ ├── __init__.py
│ ├── aio
│ │ ├── __init__.py
│ │ ├── grpc_interceptor.py
│ │ ├── client.py
│ │ └── retry_manager.py
│ ├── sync
│ │ ├── __init__.py
│ │ ├── grpc_interceptor.py
│ │ ├── client.py
│ │ └── retry_manager.py
│ ├── settings_protocol.py
│ ├── settings.py
│ └── base_retry_manager.py
│ ├── sandbox
│ ├── __init__.py
│ ├── client.py
│ └── async_client.py
│ ├── strategies
│ ├── __init__.py
│ ├── base
│ │ ├── __init__.py
│ │ ├── trader_interface.py
│ │ ├── models.py
│ │ ├── strategy_interface.py
│ │ ├── event.py
│ │ ├── strategy_settings_base.py
│ │ ├── errors.py
│ │ ├── strategy_supervisor.py
│ │ ├── signal.py
│ │ ├── account_manager.py
│ │ ├── signal_executor_base.py
│ │ └── trader_base.py
│ ├── plotting
│ │ ├── __init__.py
│ │ └── plotter.py
│ └── moving_average
│ │ ├── __init__.py
│ │ ├── strategy_settings.py
│ │ ├── strategy_state.py
│ │ ├── supervisor.py
│ │ └── signal_executor.py
│ ├── market_data_stream
│ ├── __init__.py
│ ├── typevars.py
│ ├── market_data_stream_interface.py
│ ├── market_data_stream_manager.py
│ └── async_market_data_stream_manager.py
│ ├── typedefs.py
│ ├── metadata.py
│ ├── constants.py
│ ├── candle_getter_protocol.py
│ ├── exceptions.py
│ ├── channels.py
│ └── clients.py
├── docs
├── api
│ └── clients.md
└── robots.md
├── CHANGELOG.md
├── .github
├── PULL_REQUEST_TEMPLATE.md
├── workflows
│ ├── check_pr_title.yml
│ ├── pypi.yml
│ ├── github_pages.yml
│ ├── bumpversion.yml
│ └── check.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.yaml
│ ├── issue.yaml
│ └── bug_report.yaml
├── pytest.ini
├── BREAKING_CHANGES.md
├── conftest.py
├── mkdocs.yml
├── README.md
├── Makefile
├── .gitignore
└── pyproject.toml
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/caches/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/marketdata/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/data_loaders/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_strategies/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/grpc/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/sandbox/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_orders_canceling/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/aio/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/sync/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/market_data_stream/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/plotting/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_strategies/test_moving_average/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/instruments_cache/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/market_data_cache/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/moving_average/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/api/clients.md:
--------------------------------------------------------------------------------
1 |
2 | # Clients
3 |
4 | ::: tinkoff.invest.clients
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | You can see [the commits](https://github.com/RussianInvestments/invest-python/commits/main)
4 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/trader_interface.py:
--------------------------------------------------------------------------------
1 | from typing import Protocol
2 |
3 |
4 | class ITrader(Protocol):
5 | def trade(self):
6 | pass
7 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/market_data_cache/datetime_range.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Tuple
3 |
4 | DatetimeRange = Tuple[datetime, datetime]
5 |
--------------------------------------------------------------------------------
/tinkoff/invest/market_data_stream/typevars.py:
--------------------------------------------------------------------------------
1 | from typing import TypeVar
2 |
3 | TMarketDataStreamManager = TypeVar("TMarketDataStreamManager")
4 | TInstrument = TypeVar("TInstrument")
5 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/settings_protocol.py:
--------------------------------------------------------------------------------
1 | from typing import Protocol
2 |
3 |
4 | class RetryClientSettingsProtocol(Protocol):
5 | use_retry: bool
6 | max_retry_attempt: int
7 |
--------------------------------------------------------------------------------
/tinkoff/invest/grpc/common_pb2_grpc.py:
--------------------------------------------------------------------------------
1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2 | """Client and server classes corresponding to protobuf-defined services."""
3 | import grpc
4 |
5 |
--------------------------------------------------------------------------------
/tinkoff/invest/typedefs.py:
--------------------------------------------------------------------------------
1 | from typing import Any, NewType, Sequence, Tuple
2 |
3 | AccountId = NewType("AccountId", str)
4 | ShareId = NewType("ShareId", str)
5 | ChannelArgumentType = Sequence[Tuple[str, Any]]
6 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/instruments_cache/settings.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from datetime import timedelta
3 |
4 |
5 | @dataclasses.dataclass()
6 | class InstrumentsCacheSettings:
7 | ttl: timedelta = timedelta(days=1)
8 |
--------------------------------------------------------------------------------
/tinkoff/invest/grpc/google/api/field_behavior_pb2_grpc.py:
--------------------------------------------------------------------------------
1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2 | """Client and server classes corresponding to protobuf-defined services."""
3 | import grpc
4 |
5 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | cache_dir = .pytest_cache
3 | asyncio_mode = auto
4 | addopts = --verbosity=2 --showlocals --strict-markers --log-level=DEBUG
5 | markers =
6 | test_sandbox: marks sandbox tests (use option '--test-sandbox' to run)
7 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | Cначала нужно добавить токен в переменную окружения.
2 |
3 |
4 |
5 | ```console
6 | $ export INVEST_TOKEN=YOUR_TOKEN
7 | ```
8 |
9 | А потом можно запускать примеры
10 |
11 | ```console
12 | $ python examples/client.py
13 | ```
14 |
--------------------------------------------------------------------------------
/examples/client.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with Client(TOKEN) as client:
10 | print(client.users.get_accounts())
11 |
12 |
13 | if __name__ == "__main__":
14 | main()
15 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/instruments_cache/models.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 |
4 | class InstrumentResponse:
5 | class_code: str
6 |
7 | figi: str
8 | ticker: str
9 | uid: str
10 |
11 |
12 | class InstrumentsResponse:
13 | instruments: List[InstrumentResponse]
14 |
--------------------------------------------------------------------------------
/examples/get_trading_statuses.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 |
5 | token = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | with Client(token) as client:
9 | statuses = client.market_data.get_trading_statuses(instrument_ids=["BBG004730N88"])
10 | print(statuses)
11 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/market_data_cache/serialization.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | def custom_asdict_factory(data):
5 | def convert_value(obj):
6 | if isinstance(obj, Enum):
7 | return obj.value
8 | return obj
9 |
10 | return {k: convert_value(v) for k, v in data}
11 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/settings.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 |
3 | from tinkoff.invest.retrying.settings_protocol import RetryClientSettingsProtocol
4 |
5 |
6 | @dataclasses.dataclass()
7 | class RetryClientSettings(RetryClientSettingsProtocol):
8 | use_retry: bool = True
9 | max_retry_attempt: int = 3
10 |
--------------------------------------------------------------------------------
/examples/sandbox_client.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest.sandbox.client import SandboxClient
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with SandboxClient(TOKEN) as client:
10 | print(client.users.get_info())
11 |
12 |
13 | if __name__ == "__main__":
14 | main()
15 |
--------------------------------------------------------------------------------
/examples/users/get_user_info.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with Client(TOKEN) as client:
10 | response = client.users.get_info()
11 | print(response)
12 |
13 |
14 | if __name__ == "__main__":
15 | main()
16 |
--------------------------------------------------------------------------------
/examples/users/get_bank_accounts.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 |
5 |
6 | def main():
7 | token = os.environ["INVEST_TOKEN"]
8 |
9 | with Client(token) as client:
10 | response = client.users.get_bank_accounts()
11 | print(response)
12 |
13 |
14 | if __name__ == "__main__":
15 | main()
16 |
--------------------------------------------------------------------------------
/examples/async_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | async def main():
10 | async with AsyncClient(TOKEN) as client:
11 | print(await client.users.get_accounts())
12 |
13 |
14 | if __name__ == "__main__":
15 | asyncio.run(main())
16 |
--------------------------------------------------------------------------------
/tinkoff/invest/metadata.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, Tuple
2 |
3 | from .constants import APP_NAME
4 |
5 |
6 | def get_metadata(token: str, app_name: Optional[str] = None) -> List[Tuple[str, str]]:
7 | if not app_name:
8 | app_name = APP_NAME
9 |
10 | return [("authorization", f"Bearer {token}"), ("x-app-name", app_name)]
11 |
--------------------------------------------------------------------------------
/examples/instruments/options.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with Client(TOKEN) as client:
10 | r = client.instruments.options()
11 | for instrument in r.instruments:
12 | print(instrument)
13 |
14 |
15 | if __name__ == "__main__":
16 | main()
17 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/overrides.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from cachetools import TTLCache as TTLCacheBase
4 |
5 |
6 | class TTLCache(TTLCacheBase):
7 | def __init__(self, maxsize, ttl, timer=None, getsizeof=None):
8 | if timer is None:
9 | timer = time.monotonic
10 | super().__init__(maxsize=maxsize, ttl=ttl, timer=timer, getsizeof=getsizeof)
11 |
--------------------------------------------------------------------------------
/tinkoff/invest/sandbox/client.py:
--------------------------------------------------------------------------------
1 | from tinkoff.invest import Client
2 | from tinkoff.invest.constants import INVEST_GRPC_API_SANDBOX
3 |
4 |
5 | class SandboxClient(Client):
6 | def __init__(
7 | self,
8 | token: str,
9 | **kwargs,
10 | ):
11 | kwargs["target"] = INVEST_GRPC_API_SANDBOX
12 | super().__init__(token, **kwargs)
13 |
--------------------------------------------------------------------------------
/examples/instruments/instruments.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with Client(TOKEN) as client:
10 | r = client.instruments.find_instrument(query="BBG001M2SC01")
11 | for i in r.instruments:
12 | print(i)
13 |
14 |
15 | if __name__ == "__main__":
16 | main()
17 |
--------------------------------------------------------------------------------
/examples/open_sandbox_account.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest.sandbox.client import SandboxClient
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with SandboxClient(TOKEN) as client:
10 | print(client.sandbox.open_sandbox_account(name="tcs"))
11 | print(client.users.get_accounts())
12 |
13 |
14 | if __name__ == "__main__":
15 | main()
16 |
--------------------------------------------------------------------------------
/tinkoff/invest/sandbox/async_client.py:
--------------------------------------------------------------------------------
1 | from tinkoff.invest import AsyncClient
2 | from tinkoff.invest.constants import INVEST_GRPC_API_SANDBOX
3 |
4 |
5 | class AsyncSandboxClient(AsyncClient):
6 | def __init__(
7 | self,
8 | token: str,
9 | **kwargs,
10 | ):
11 | kwargs["target"] = INVEST_GRPC_API_SANDBOX
12 | super().__init__(token, **kwargs)
13 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/moving_average/strategy_settings.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from datetime import timedelta
3 |
4 | from tinkoff.invest.strategies.base.strategy_settings_base import StrategySettings
5 |
6 |
7 | @dataclasses.dataclass
8 | class MovingAverageStrategySettings(StrategySettings):
9 | long_period: timedelta
10 | short_period: timedelta
11 | std_period: timedelta
12 |
--------------------------------------------------------------------------------
/.github/workflows/check_pr_title.yml:
--------------------------------------------------------------------------------
1 | name: Check PR title
2 | on:
3 | pull_request_target:
4 | types:
5 | - opened
6 | - reopened
7 | - edited
8 | - synchronize
9 |
10 | jobs:
11 | lint:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: aslafy-z/conventional-pr-title-action@v3
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 |
--------------------------------------------------------------------------------
/examples/users/async_get_bank_accounts.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 |
6 |
7 | async def main():
8 | token = os.environ["INVEST_TOKEN"]
9 |
10 | async with AsyncClient(token) as client:
11 | response = await client.users.get_bank_accounts()
12 | print(response)
13 |
14 |
15 | if __name__ == "__main__":
16 | asyncio.run(main())
17 |
--------------------------------------------------------------------------------
/examples/instruments/get_brands.py:
--------------------------------------------------------------------------------
1 | """Example - How to get Brands"""
2 |
3 | import os
4 |
5 | from tinkoff.invest import Client
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | def main():
11 | with Client(TOKEN) as client:
12 | r = client.instruments.get_brands()
13 | for brand in r.brands:
14 | print(brand)
15 |
16 |
17 | if __name__ == "__main__":
18 | main()
19 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/market_data_cache/instrument_date_range_market_data.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from typing import Iterable
3 |
4 | from tinkoff.invest.caching.market_data_cache.datetime_range import DatetimeRange
5 | from tinkoff.invest.schemas import HistoricCandle
6 |
7 |
8 | @dataclasses.dataclass()
9 | class InstrumentDateRangeData:
10 | date_range: DatetimeRange
11 | historic_candles: Iterable[HistoricCandle]
12 |
--------------------------------------------------------------------------------
/BREAKING_CHANGES.md:
--------------------------------------------------------------------------------
1 | # Breaking changes
2 | ## 0.2.0-beta60
3 | - `MarketDataCache` was moved to [tinkoff/invest/caching/market_data_cache/cache.py](tinkoff/invest/caching/market_data_cache/cache.py).
4 | - The correct import is now `from tinkoff.invest.caching.market_data_cache.cache import MarketDataCache` instead of `from tinkoff.invest.services import MarketDataCache`.
5 | - Import in [download_all_candles.py](examples/download_all_candles.py) was also corrected.
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/models.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from datetime import datetime
3 | from decimal import Decimal
4 |
5 |
6 | @dataclass(eq=False, repr=True)
7 | class Candle:
8 | open: Decimal
9 | high: Decimal
10 | low: Decimal
11 | close: Decimal
12 |
13 |
14 | @dataclass(eq=False, repr=True)
15 | class CandleEvent:
16 | candle: Candle
17 | volume: int
18 | time: datetime
19 | is_complete: bool
20 |
--------------------------------------------------------------------------------
/examples/async_get_trading_statuses.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 |
6 | token = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | async def main():
10 | async with AsyncClient(token) as client:
11 | statuses = await client.market_data.get_trading_statuses(
12 | instrument_ids=["BBG004730N88"]
13 | )
14 | print(statuses)
15 |
16 |
17 | if __name__ == "__main__":
18 | asyncio.run(main())
19 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/market_data_cache/interface.py:
--------------------------------------------------------------------------------
1 | from typing import Protocol, TypeVar
2 |
3 | from tinkoff.invest.caching.market_data_cache.datetime_range import DatetimeRange
4 |
5 | TInstrumentData = TypeVar("TInstrumentData")
6 |
7 |
8 | class IInstrumentMarketDataStorage(Protocol[TInstrumentData]):
9 | def get(self, request_range: DatetimeRange) -> TInstrumentData:
10 | pass
11 |
12 | def update(self, data_list: TInstrumentData):
13 | pass
14 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/instruments_cache/protocol.py:
--------------------------------------------------------------------------------
1 | from typing import Protocol
2 |
3 | from tinkoff.invest import InstrumentStatus
4 | from tinkoff.invest.caching.instruments_cache.models import InstrumentsResponse
5 |
6 |
7 | class InstrumentsResponseCallable(Protocol):
8 | def __call__(
9 | self, *, instrument_status: InstrumentStatus = InstrumentStatus(0)
10 | ) -> InstrumentsResponse:
11 | ...
12 |
13 | def __name__(self) -> str:
14 | ...
15 |
--------------------------------------------------------------------------------
/examples/get_last_prices.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client, InstrumentStatus
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with Client(TOKEN) as client:
10 | print(
11 | client.market_data.get_last_prices(
12 | figi=["BBG004730ZJ9"],
13 | instrument_status=InstrumentStatus.INSTRUMENT_STATUS_BASE,
14 | )
15 | )
16 |
17 |
18 | if __name__ == "__main__":
19 | main()
20 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/strategy_interface.py:
--------------------------------------------------------------------------------
1 | from typing import Iterable, Protocol
2 |
3 | from tinkoff.invest.strategies.base.models import CandleEvent
4 | from tinkoff.invest.strategies.base.signal import Signal
5 |
6 |
7 | class InvestStrategy(Protocol):
8 | def fit(self, candles: Iterable[CandleEvent]) -> None:
9 | pass
10 |
11 | def observe(self, candle: CandleEvent) -> None:
12 | pass
13 |
14 | def predict(self) -> Iterable[Signal]:
15 | pass
16 |
--------------------------------------------------------------------------------
/tinkoff/invest/constants.py:
--------------------------------------------------------------------------------
1 | INVEST_GRPC_API = "invest-public-api.tinkoff.ru"
2 | INVEST_GRPC_API_SANDBOX = "sandbox-invest-public-api.tinkoff.ru"
3 | APP_VERSION = "0.2.0-beta117"
4 | APP_NAME = f"tinkoff.invest-python-{APP_VERSION}"
5 | X_TRACKING_ID = "x-tracking-id"
6 | X_RATELIMIT_LIMIT = "x-ratelimit-limit"
7 | X_RATELIMIT_REMAINING = "x-ratelimit-remaining"
8 | X_RATELIMIT_RESET = "x-ratelimit-reset"
9 | MESSAGE = "message"
10 | MEGABYTE = 1024 * 1024
11 | MAX_RECEIVE_MESSAGE_LENGTH = 10 * MEGABYTE
12 |
--------------------------------------------------------------------------------
/examples/get_strategies.py:
--------------------------------------------------------------------------------
1 | """Example - How to get Strategies"""
2 |
3 | import os
4 |
5 | from tinkoff.invest import Client
6 | from tinkoff.invest.schemas import GetStrategiesRequest
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 |
11 | def main():
12 | with Client(TOKEN) as client:
13 | r = client.signals.get_strategies(request=GetStrategiesRequest())
14 | for strategy in r.strategies:
15 | print(strategy)
16 |
17 |
18 | if __name__ == "__main__":
19 | main()
20 |
--------------------------------------------------------------------------------
/examples/porfolio_stream_client.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with Client(TOKEN) as client:
10 | accounts = client.users.get_accounts()
11 | for portfolio in client.operations_stream.portfolio_stream(
12 | accounts=[acc.id for acc in accounts.accounts], ping_delay_ms=60_000
13 | ):
14 | print(portfolio)
15 |
16 |
17 | if __name__ == "__main__":
18 | main()
19 |
--------------------------------------------------------------------------------
/tinkoff/invest/candle_getter_protocol.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Generator, Optional, Protocol
3 |
4 | from tinkoff.invest import CandleInterval, HistoricCandle
5 |
6 |
7 | class CandleGetter(Protocol):
8 | def get_all_candles( # pragma: no cover
9 | self,
10 | *,
11 | from_: datetime,
12 | to: Optional[datetime],
13 | interval: CandleInterval,
14 | figi: str,
15 | ) -> Generator[HistoricCandle, None, None]:
16 | pass
17 |
--------------------------------------------------------------------------------
/examples/instruments/instrument_find_by_ticker.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client, InstrumentIdType
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with Client(TOKEN) as client:
10 | r = client.instruments.get_instrument_by(
11 | id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER,
12 | id="LKOH",
13 | class_code="TQBR",
14 | )
15 | print(r.instrument)
16 |
17 |
18 | if __name__ == "__main__":
19 | main()
20 |
--------------------------------------------------------------------------------
/examples/instruments/indicatives.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import IndicativesRequest
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | request = IndicativesRequest()
12 | indicatives = client.instruments.indicatives(request=request)
13 | for instrument in indicatives.instruments:
14 | print(instrument.name)
15 |
16 |
17 | if __name__ == "__main__":
18 | main()
19 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/event.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from datetime import datetime
3 |
4 | from tinkoff.invest.strategies.base.models import CandleEvent
5 | from tinkoff.invest.strategies.base.signal import Signal
6 |
7 |
8 | @dataclass
9 | class StrategyEvent:
10 | time: datetime
11 |
12 |
13 | @dataclass
14 | class DataEvent(StrategyEvent):
15 | candle_event: CandleEvent
16 |
17 |
18 | @dataclass
19 | class SignalEvent(StrategyEvent):
20 | signal: Signal
21 | was_executed: bool
22 |
--------------------------------------------------------------------------------
/examples/get_last_trades.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import TradeSourceType
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | print(
12 | client.market_data.get_last_trades(
13 | instrument_id="BBG004730ZJ9",
14 | trade_source=TradeSourceType.TRADE_SOURCE_EXCHANGE,
15 | )
16 | )
17 |
18 |
19 | if __name__ == "__main__":
20 | main()
21 |
--------------------------------------------------------------------------------
/examples/instruments/get_bonds.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import InstrumentExchangeType
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | r = client.instruments.bonds(
12 | instrument_exchange=InstrumentExchangeType.INSTRUMENT_EXCHANGE_UNSPECIFIED
13 | )
14 | for bond in r.instruments:
15 | print(bond)
16 |
17 |
18 | if __name__ == "__main__":
19 | main()
20 |
--------------------------------------------------------------------------------
/examples/instruments/get_asset_fundamentals.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import GetAssetFundamentalsRequest
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | request = GetAssetFundamentalsRequest(
12 | assets=["40d89385-a03a-4659-bf4e-d3ecba011782"],
13 | )
14 | print(client.instruments.get_asset_fundamentals(request=request))
15 |
16 |
17 | if __name__ == "__main__":
18 | main()
19 |
--------------------------------------------------------------------------------
/examples/order_state_stream.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import OrderStateStreamRequest
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | request = OrderStateStreamRequest()
12 | request.ping_delay_millis = 10000
13 | stream = client.orders_stream.order_state_stream(request=request)
14 | for order_state in stream:
15 | print(order_state)
16 |
17 |
18 | if __name__ == "__main__":
19 | main()
20 |
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | def pytest_addoption(parser):
5 | parser.addoption(
6 | "--test-sandbox",
7 | action="store_true",
8 | default=False,
9 | help="Run sandbox tests",
10 | )
11 |
12 |
13 | def pytest_collection_modifyitems(config, items):
14 | if not config.getoption("--test-sandbox"):
15 | skipper = pytest.mark.skip(reason="Only run when --test-sandbox is given")
16 | for item in items:
17 | if "test_sandbox" in item.keywords:
18 | item.add_marker(skipper)
19 |
--------------------------------------------------------------------------------
/examples/async_get_strategies.py:
--------------------------------------------------------------------------------
1 | """Example - How to get all strategies"""
2 | import asyncio
3 | import os
4 |
5 | from tinkoff.invest import AsyncClient
6 | from tinkoff.invest.schemas import GetStrategiesRequest
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 |
11 | async def main():
12 | async with AsyncClient(TOKEN) as client:
13 | r = await client.signals.get_strategies(request=GetStrategiesRequest())
14 | for strategy in r.strategies:
15 | print(strategy)
16 |
17 |
18 | if __name__ == "__main__":
19 | asyncio.run(main())
20 |
--------------------------------------------------------------------------------
/examples/async_indicatives.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import IndicativesRequest
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | request = IndicativesRequest()
13 | indicatives = await client.instruments.indicatives(request=request)
14 | for instrument in indicatives.instruments:
15 | print(instrument.name)
16 |
17 |
18 | if __name__ == "__main__":
19 | asyncio.run(main())
20 |
--------------------------------------------------------------------------------
/examples/async_order_state_stream.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import OrderStateStreamRequest
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | request = OrderStateStreamRequest()
13 | stream = client.orders_stream.order_state_stream(request=request)
14 | async for order_state in stream:
15 | print(order_state)
16 |
17 |
18 | if __name__ == "__main__":
19 | asyncio.run(main())
20 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish to PYPI
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 | release:
8 | types:
9 | - created
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 |
18 | - name: Install poetry
19 | run: make install-poetry
20 |
21 | - name: Publish package to pypi
22 | run: make publish
23 | env:
24 | pypi_username: ${{ secrets.PYPI_USERNAME }}
25 | pypi_password: ${{ secrets.PYPI_PASSWORD }}
26 |
--------------------------------------------------------------------------------
/examples/async_get_last_prices.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient, InstrumentStatus
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | async def main():
10 | async with AsyncClient(TOKEN) as client:
11 | print(
12 | await client.market_data.get_last_prices(
13 | figi=["BBG004730ZJ9"],
14 | instrument_status=InstrumentStatus.INSTRUMENT_STATUS_ALL,
15 | )
16 | ) # pylint:disable=line-too-long
17 |
18 |
19 | if __name__ == "__main__":
20 | asyncio.run(main())
21 |
--------------------------------------------------------------------------------
/examples/positions_stream.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 |
5 | TOKEN = os.environ["INVEST_TOKEN"]
6 |
7 |
8 | def main():
9 | with Client(TOKEN) as client:
10 | response = client.users.get_accounts()
11 | accounts = [account.id for account in response.accounts]
12 | for response in client.operations_stream.positions_stream(
13 | accounts=accounts, with_initial_positions=True
14 | ): # noqa:E501 # pylint:disable=line-too-long
15 | print(response)
16 |
17 |
18 | if __name__ == "__main__":
19 | main()
20 |
--------------------------------------------------------------------------------
/.github/workflows/github_pages.yml:
--------------------------------------------------------------------------------
1 | name: Github pages
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Install python dependencies
15 | run: make install-poetry install-docs
16 |
17 | - name: Generate docs
18 | run: make docs
19 |
20 | - name: Deploy pages
21 | uses: peaceiris/actions-gh-pages@v3
22 | with:
23 | github_token: ${{ secrets.GITHUB_TOKEN }}
24 | publish_dir: ./site
25 |
--------------------------------------------------------------------------------
/examples/instruments/async_get_bonds.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import InstrumentExchangeType
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | bonds = await client.instruments.bonds(
13 | instrument_exchange=InstrumentExchangeType.INSTRUMENT_EXCHANGE_UNSPECIFIED,
14 | )
15 | for bond in bonds.instruments:
16 | print(bond)
17 |
18 |
19 | if __name__ == "__main__":
20 | asyncio.run(main())
21 |
--------------------------------------------------------------------------------
/examples/get_candles_with_limit.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import CandleInterval, Client
4 | from tinkoff.invest.utils import now
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | for candle in client.market_data.get_candles(
12 | instrument_id="BBG004730N88",
13 | to=now(),
14 | limit=24,
15 | interval=CandleInterval.CANDLE_INTERVAL_HOUR,
16 | ).candles:
17 | print(candle)
18 |
19 | return 0
20 |
21 |
22 | if __name__ == "__main__":
23 | main()
24 |
--------------------------------------------------------------------------------
/examples/instruments/structured_notes.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import InstrumentsRequest, InstrumentStatus
5 |
6 |
7 | def main():
8 | token = os.environ["INVEST_TOKEN"]
9 |
10 | with Client(token) as client:
11 | r = client.instruments.structured_notes(
12 | request=InstrumentsRequest(
13 | instrument_status=InstrumentStatus.INSTRUMENT_STATUS_ALL
14 | )
15 | )
16 | for note in r.instruments:
17 | print(note)
18 |
19 |
20 | if __name__ == "__main__":
21 | main()
22 |
--------------------------------------------------------------------------------
/examples/get_sandbox_max_lots.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import GetMaxLotsRequest
4 | from tinkoff.invest.sandbox.client import SandboxClient
5 |
6 | TOKEN = os.environ["INVEST_SANDBOX_TOKEN"]
7 |
8 |
9 | def main():
10 | with SandboxClient(TOKEN) as client:
11 | account_id = client.users.get_accounts().accounts[0].id
12 | request = GetMaxLotsRequest(
13 | account_id=account_id,
14 | instrument_id="BBG004730N88",
15 | )
16 | print(client.sandbox.get_sandbox_max_lots(request=request))
17 |
18 |
19 | if __name__ == "__main__":
20 | main()
21 |
--------------------------------------------------------------------------------
/examples/instruments/structured_notes_by.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import InstrumentIdType, InstrumentRequest
5 |
6 |
7 | def main():
8 | token = os.environ["INVEST_TOKEN"]
9 |
10 | with Client(token) as client:
11 | r = client.instruments.structured_note_by(
12 | request=InstrumentRequest(
13 | id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_UID,
14 | id="1d7dfabb-9e82-4de4-8add-8475db83d2bd",
15 | )
16 | )
17 | print(r.instrument)
18 |
19 |
20 | if __name__ == "__main__":
21 | main()
22 |
--------------------------------------------------------------------------------
/examples/get_risk_rates.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import RiskRatesRequest
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | request = RiskRatesRequest()
12 | request.instrument_id = ["BBG001M2SC01", "BBG004730N88"]
13 | r = client.instruments.get_risk_rates(request=request)
14 | for i in r.instrument_risk_rates:
15 | print(i.instrument_uid)
16 | print(i.short_risk_rate)
17 | print(i.long_risk_rate)
18 |
19 |
20 | if __name__ == "__main__":
21 | main()
22 |
--------------------------------------------------------------------------------
/examples/instruments/async_structured_notes_by.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import InstrumentIdType, InstrumentRequest
6 |
7 |
8 | async def main():
9 | token = os.environ["INVEST_TOKEN"]
10 |
11 | with AsyncClient(token) as client:
12 | r = await client.instruments.structured_note_by(
13 | request=InstrumentRequest(
14 | id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id="BBG012S2DCJ8"
15 | )
16 | )
17 | print(r.instrument)
18 |
19 |
20 | if __name__ == "__main__":
21 | asyncio.run(main())
22 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 |
3 | import pytest
4 |
5 |
6 | def skip_when(
7 | exception_type,
8 | is_error_message_expected,
9 | reason="Skipping because of the exception",
10 | ):
11 | def decorator_func(f):
12 | @wraps(f)
13 | def wrapper(*args, **kwargs):
14 | try:
15 | return f(*args, **kwargs)
16 | except exception_type as error:
17 | if is_error_message_expected(str(error)):
18 | pytest.skip(reason)
19 | else:
20 | raise error
21 |
22 | return wrapper
23 |
24 | return decorator_func
25 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/strategy_settings_base.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from datetime import timedelta
3 | from decimal import Decimal
4 |
5 | from tinkoff.invest import CandleInterval
6 | from tinkoff.invest.typedefs import AccountId, ShareId
7 | from tinkoff.invest.utils import candle_interval_to_timedelta
8 |
9 |
10 | @dataclasses.dataclass
11 | class StrategySettings:
12 | share_id: ShareId
13 | account_id: AccountId
14 | max_transaction_price: Decimal
15 | candle_interval: CandleInterval
16 |
17 | @property
18 | def candle_interval_timedelta(self) -> timedelta:
19 | return candle_interval_to_timedelta(self.candle_interval)
20 |
--------------------------------------------------------------------------------
/examples/instruments/async_structured_notes.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import InstrumentsRequest, InstrumentStatus
6 |
7 |
8 | async def main():
9 | token = os.environ["INVEST_TOKEN"]
10 |
11 | with AsyncClient(token) as client:
12 | r = await client.instruments.structured_notes(
13 | request=InstrumentsRequest(
14 | instrument_status=InstrumentStatus.INSTRUMENT_STATUS_ALL
15 | )
16 | )
17 | for note in r.instruments:
18 | print(note)
19 |
20 |
21 | if __name__ == "__main__":
22 | asyncio.run(main())
23 |
--------------------------------------------------------------------------------
/tests/test_datetime_utils.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | import pytest
4 |
5 | from tinkoff.invest import CandleInterval
6 | from tinkoff.invest.utils import (
7 | candle_interval_to_timedelta,
8 | ceil_datetime,
9 | floor_datetime,
10 | now,
11 | )
12 |
13 |
14 | @pytest.fixture(params=[i.value for i in CandleInterval])
15 | def interval(request) -> timedelta:
16 | return candle_interval_to_timedelta(request.param)
17 |
18 |
19 | def test_floor_ceil(interval: timedelta):
20 | now_ = now()
21 |
22 | a, b = floor_datetime(now_, interval), ceil_datetime(now_, interval)
23 |
24 | assert a < b
25 | assert b - a == interval
26 |
--------------------------------------------------------------------------------
/examples/async_get_candles_with_limit.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient, CandleInterval
5 | from tinkoff.invest.utils import now
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | candles = await client.market_data.get_candles(
13 | instrument_id="BBG004730N88",
14 | to=now(),
15 | limit=3,
16 | interval=CandleInterval.CANDLE_INTERVAL_HOUR,
17 | )
18 | for candle in candles.candles:
19 | print(candle)
20 |
21 | return 0
22 |
23 |
24 | if __name__ == "__main__":
25 | asyncio.run(main())
26 |
--------------------------------------------------------------------------------
/examples/async_get_risk_rates.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import RiskRatesRequest
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | request = RiskRatesRequest()
13 | request.instrument_id = ["BBG001M2SC01", "BBG004730N88"]
14 | r = await client.instruments.get_risk_rates(request=request)
15 | for i in r.instrument_risk_rates:
16 | print(i.instrument_uid)
17 | print(i.short_risk_rate)
18 | print(i.long_risk_rate)
19 |
20 |
21 | if __name__ == "__main__":
22 | asyncio.run(main())
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yaml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Предложите идею для этого проекта
3 | title: "[Feature] Title"
4 | labels: ["enhancement"]
5 | body:
6 | - type: textarea
7 | id: description
8 | attributes:
9 | label: Описание
10 | validations:
11 | required: true
12 | - type: textarea
13 | id: to-resolve
14 | attributes:
15 | label: Желаемое решение
16 | description: Что нужно сделать?
17 | validations:
18 | required: false
19 | - type: textarea
20 | id: additional
21 | attributes:
22 | label: Дополнительно
23 | description: Фрагменты кода, описание апи, ...
24 | validations:
25 | required: false
26 |
--------------------------------------------------------------------------------
/examples/get_signals.py:
--------------------------------------------------------------------------------
1 | """Example - How to get Signals with filtering"""
2 |
3 | import os
4 |
5 | from tinkoff.invest import Client
6 | from tinkoff.invest.schemas import GetSignalsRequest, SignalState
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 |
11 | def main():
12 | with Client(TOKEN) as client:
13 | request = GetSignalsRequest()
14 | request.instrument_uid = "e6123145-9665-43e0-8413-cd61b8aa9b13" # Сбербанк
15 | request.active = SignalState.SIGNAL_STATE_ACTIVE # только активные сигналы
16 | r = client.signals.get_signals(request=request)
17 | for signal in r.signals:
18 | print(signal)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/examples/cancel_orders.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | from tinkoff.invest import Client
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 | logger = logging.getLogger(__name__)
9 | logging.basicConfig(level=logging.INFO)
10 |
11 |
12 | def main():
13 | with Client(TOKEN) as client:
14 | response = client.users.get_accounts()
15 | account, *_ = response.accounts
16 | account_id = account.id
17 | logger.info("Orders: %s", client.orders.get_orders(account_id=account_id))
18 | client.cancel_all_orders(account_id=account.id)
19 | logger.info("Orders: %s", client.orders.get_orders(account_id=account_id))
20 |
21 |
22 | if __name__ == "__main__":
23 | main()
24 |
--------------------------------------------------------------------------------
/examples/all_candles.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import timedelta
3 |
4 | from tinkoff.invest import CandleInterval, Client
5 | from tinkoff.invest.schemas import CandleSource
6 | from tinkoff.invest.utils import now
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 |
11 | def main():
12 | with Client(TOKEN) as client:
13 | for candle in client.get_all_candles(
14 | instrument_id="BBG004730N88",
15 | from_=now() - timedelta(days=365),
16 | interval=CandleInterval.CANDLE_INTERVAL_HOUR,
17 | candle_source_type=CandleSource.CANDLE_SOURCE_UNSPECIFIED,
18 | ):
19 | print(candle)
20 |
21 | return 0
22 |
23 |
24 | if __name__ == "__main__":
25 | main()
26 |
--------------------------------------------------------------------------------
/examples/instruments/async_get_consensus_forecasts.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import GetConsensusForecastsRequest, Page
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | request = GetConsensusForecastsRequest(
13 | paging=Page(page_number=0, limit=2),
14 | )
15 | response = await client.instruments.get_consensus_forecasts(request=request)
16 | print(response.page)
17 | for forecast in response.items:
18 | print(forecast.uid, forecast.consensus.name)
19 |
20 |
21 | if __name__ == "__main__":
22 | asyncio.run(main())
23 |
--------------------------------------------------------------------------------
/tests/test_signals.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=redefined-outer-name,unused-variable
2 |
3 | from unittest import mock
4 |
5 | import pytest
6 |
7 | from tinkoff.invest.services import SignalService
8 |
9 |
10 | @pytest.fixture()
11 | def signals_service():
12 | return mock.create_autospec(spec=SignalService)
13 |
14 |
15 | def test_get_signals(signals_service):
16 | response = signals_service.get_signals(request=mock.Mock()) # noqa: F841
17 | signals_service.get_signals.assert_called_once_with(request=mock.ANY)
18 |
19 |
20 | def test_get_strategies(signals_service):
21 | response = signals_service.get_strategies(request=mock.Mock()) # noqa: F841
22 | signals_service.get_strategies.assert_called_once_with(request=mock.ANY)
23 |
--------------------------------------------------------------------------------
/examples/get_active_orders.py:
--------------------------------------------------------------------------------
1 | """Example - How to get list of active orders."""
2 |
3 | import logging
4 | import os
5 |
6 | from tinkoff.invest import Client
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 | logger = logging.getLogger(__name__)
11 | logging.basicConfig(level=logging.INFO)
12 |
13 |
14 | def main():
15 | with Client(TOKEN) as client:
16 | response = client.users.get_accounts()
17 | account, *_ = response.accounts
18 | account_id = account.id
19 |
20 | orders = client.orders.get_orders(
21 | account_id=account_id,
22 | )
23 | print("Active orders:")
24 | for order in orders.orders:
25 | print(order)
26 |
27 |
28 | if __name__ == "__main__":
29 | main()
30 |
--------------------------------------------------------------------------------
/examples/async_get_sandbox_max_lots.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import GetMaxLotsRequest
5 | from tinkoff.invest.sandbox.async_client import AsyncSandboxClient
6 | from tinkoff.invest.sandbox.client import SandboxClient
7 |
8 | TOKEN = os.environ["INVEST_SANDBOX_TOKEN"]
9 |
10 |
11 | async def main():
12 | async with AsyncSandboxClient(TOKEN) as client:
13 | account_id = (await client.users.get_accounts()).accounts[0].id
14 | request = GetMaxLotsRequest(
15 | account_id=account_id,
16 | instrument_id="BBG004730N88",
17 | )
18 | print(await client.sandbox.get_sandbox_max_lots(request=request))
19 |
20 |
21 | if __name__ == "__main__":
22 | asyncio.run(main())
23 |
--------------------------------------------------------------------------------
/examples/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | from tinkoff.invest import Client, RequestError
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 | logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.INFO)
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | def main():
13 | with Client(TOKEN) as client:
14 | _ = client.users.get_accounts().accounts
15 | try:
16 | client.users.get_margin_attributes(account_id="123")
17 | except RequestError as err:
18 | tracking_id = err.metadata.tracking_id if err.metadata else ""
19 | logger.error("Error tracking_id=%s code=%s", tracking_id, str(err.code))
20 |
21 |
22 | if __name__ == "__main__":
23 | main()
24 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/errors.py:
--------------------------------------------------------------------------------
1 | class StrategyError(Exception):
2 | pass
3 |
4 |
5 | class NotEnoughData(StrategyError):
6 | pass
7 |
8 |
9 | class MarginalTradeIsNotActive(StrategyError):
10 | pass
11 |
12 |
13 | class InsufficientMarginalTradeFunds(StrategyError):
14 | pass
15 |
16 |
17 | class CandleEventForDateNotFound(StrategyError):
18 | pass
19 |
20 |
21 | class UnknownSignal(StrategyError):
22 | pass
23 |
24 |
25 | class OldCandleObservingError(StrategyError):
26 | pass
27 |
28 |
29 | class MarketDataNotAvailableError(StrategyError):
30 | pass
31 |
32 |
33 | class StrategySupervisorError(Exception):
34 | pass
35 |
36 |
37 | class EventsWereNotSupervised(StrategySupervisorError):
38 | pass
39 |
--------------------------------------------------------------------------------
/scripts/update_package_version.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 |
4 |
5 | def set_version(new_value: str, constant_name: str, file_path: str) -> None:
6 | with open(file_path, "r") as file:
7 | file_data = file.read()
8 |
9 | constant_pattern = re.compile(rf'{constant_name}\s*=\s*["\'].*?["\']', re.MULTILINE)
10 | file_data = constant_pattern.sub(f'{constant_name} = "{new_value}"', file_data)
11 |
12 | with open(file_path, "w") as file:
13 | file.write(file_data)
14 |
15 |
16 | def main() -> None:
17 | version = sys.argv[1]
18 | set_version(version, "__version__", "tinkoff/invest/__init__.py")
19 | set_version(version, "APP_VERSION", "tinkoff/invest/constants.py")
20 |
21 |
22 | if __name__ == "__main__":
23 | main()
24 |
--------------------------------------------------------------------------------
/examples/instruments/get_bond_events.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import EventType, GetBondEventsRequest, InstrumentType
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | bond = client.instruments.find_instrument(
12 | query="Тинькофф Банк выпуск 1",
13 | instrument_kind=InstrumentType.INSTRUMENT_TYPE_BOND,
14 | ).instruments[0]
15 |
16 | request = GetBondEventsRequest(
17 | instrument_id=bond.uid,
18 | type=EventType.EVENT_TYPE_CALL,
19 | )
20 | print(client.instruments.get_bond_events(request=request))
21 |
22 |
23 | if __name__ == "__main__":
24 | main()
25 |
--------------------------------------------------------------------------------
/examples/async_all_candles.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | from datetime import timedelta
4 |
5 | from tinkoff.invest import AsyncClient, CandleInterval
6 | from tinkoff.invest.schemas import CandleSource
7 | from tinkoff.invest.utils import now
8 |
9 | TOKEN = os.environ["INVEST_TOKEN"]
10 |
11 |
12 | async def main():
13 | async with AsyncClient(TOKEN) as client:
14 | async for candle in client.get_all_candles(
15 | instrument_id="BBG004730N88",
16 | from_=now() - timedelta(days=365),
17 | interval=CandleInterval.CANDLE_INTERVAL_HOUR,
18 | candle_source_type=CandleSource.CANDLE_SOURCE_EXCHANGE,
19 | ):
20 | print(candle)
21 |
22 |
23 | if __name__ == "__main__":
24 | asyncio.run(main())
25 |
--------------------------------------------------------------------------------
/examples/instruments/async_get_forecast_by.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import GetForecastRequest
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | instrument = (
13 | await client.instruments.find_instrument(
14 | query="Сбер Банк - привилегированные акции"
15 | )
16 | ).instruments[0]
17 | request = GetForecastRequest(instrument_id=instrument.uid)
18 | response = await client.instruments.get_forecast_by(request=request)
19 | print(instrument.name, response.consensus.recommendation.name)
20 |
21 |
22 | if __name__ == "__main__":
23 | asyncio.run(main())
24 |
--------------------------------------------------------------------------------
/examples/instruments/async_get_assets.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import AssetsRequest, InstrumentStatus, InstrumentType
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | r = await client.instruments.get_assets(
13 | request=AssetsRequest(
14 | instrument_type=InstrumentType.INSTRUMENT_TYPE_SHARE,
15 | instrument_status=InstrumentStatus.INSTRUMENT_STATUS_BASE,
16 | ) # pylint:disable=line-too-long
17 | )
18 | print("BASE SHARE ASSETS")
19 | for bond in r.assets:
20 | print(bond)
21 |
22 |
23 | if __name__ == "__main__":
24 | asyncio.run(main())
25 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/sync/grpc_interceptor.py:
--------------------------------------------------------------------------------
1 | import grpc
2 |
3 | from tinkoff.invest.retrying.sync.retry_manager import RetryManager
4 |
5 |
6 | class RetryClientInterceptor(grpc.UnaryUnaryClientInterceptor):
7 | def __init__(
8 | self, retry_manager: RetryManager
9 | ): # pylint: disable=super-init-not-called
10 | self._retry_manager = retry_manager
11 |
12 | def _intercept_with_retry(self, continuation, client_call_details, request):
13 | def call():
14 | return continuation(client_call_details, request)
15 |
16 | return self._retry_manager.call_with_retries(call=call)
17 |
18 | def intercept_unary_unary(self, continuation, client_call_details, request):
19 | return self._intercept_with_retry(continuation, client_call_details, request)
20 |
--------------------------------------------------------------------------------
/examples/get_market_values.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import GetMarketValuesRequest, MarketValueType
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | for values in client.market_data.get_market_values(
12 | request=GetMarketValuesRequest(
13 | instrument_id=["BBG004730N88"],
14 | values=[
15 | MarketValueType.INSTRUMENT_VALUE_LAST_PRICE,
16 | MarketValueType.INSTRUMENT_VALUE_CLOSE_PRICE,
17 | ],
18 | )
19 | ).instruments:
20 | for value in values.values:
21 | print(value)
22 |
23 | return 0
24 |
25 |
26 | if __name__ == "__main__":
27 | main()
28 |
--------------------------------------------------------------------------------
/examples/retrying_client.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from datetime import timedelta
4 |
5 | from tinkoff.invest import CandleInterval
6 | from tinkoff.invest.retrying.settings import RetryClientSettings
7 | from tinkoff.invest.retrying.sync.client import RetryingClient
8 | from tinkoff.invest.utils import now
9 |
10 | logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG)
11 |
12 | TOKEN = os.environ["INVEST_TOKEN"]
13 |
14 | retry_settings = RetryClientSettings(use_retry=True, max_retry_attempt=2)
15 |
16 | with RetryingClient(TOKEN, settings=retry_settings) as client:
17 | for candle in client.get_all_candles(
18 | figi="BBG000B9XRY4",
19 | from_=now() - timedelta(days=301),
20 | interval=CandleInterval.CANDLE_INTERVAL_1_MIN,
21 | ):
22 | print(candle)
23 |
--------------------------------------------------------------------------------
/examples/instruments/get_consensus_forecasts.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import timedelta
3 |
4 | from tinkoff.invest import Client
5 | from tinkoff.invest.schemas import (
6 | GetAssetReportsRequest,
7 | GetConsensusForecastsRequest,
8 | InstrumentIdType,
9 | Page,
10 | )
11 | from tinkoff.invest.utils import now
12 |
13 | TOKEN = os.environ["INVEST_TOKEN"]
14 |
15 |
16 | def main():
17 | with Client(TOKEN) as client:
18 | request = GetConsensusForecastsRequest(
19 | paging=Page(page_number=0, limit=2),
20 | )
21 | response = client.instruments.get_consensus_forecasts(request=request)
22 | print(response.page)
23 | for forecast in response.items:
24 | print(forecast.uid, forecast.consensus.name)
25 |
26 |
27 | if __name__ == "__main__":
28 | main()
29 |
--------------------------------------------------------------------------------
/examples/instruments/get_asset_reports.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import timedelta
3 |
4 | from tinkoff.invest import Client
5 | from tinkoff.invest.schemas import GetAssetReportsRequest
6 | from tinkoff.invest.utils import now
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 |
11 | def main():
12 | with Client(TOKEN) as client:
13 | instruments = client.instruments.find_instrument(
14 | query="Тинькофф Квадратные метры"
15 | )
16 | instrument = instruments.instruments[0]
17 | print(instrument.name)
18 | request = GetAssetReportsRequest(
19 | instrument_id=instrument.uid,
20 | from_=now() - timedelta(days=7),
21 | to=now(),
22 | )
23 | print(client.instruments.get_asset_reports(request=request))
24 |
25 |
26 | if __name__ == "__main__":
27 | main()
28 |
--------------------------------------------------------------------------------
/examples/instruments/get_assets.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import AssetsRequest, InstrumentStatus, InstrumentType
5 |
6 | TOKEN = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | def main():
10 | with Client(TOKEN) as client:
11 | r = client.instruments.get_assets(
12 | request=AssetsRequest(instrument_type=InstrumentType.INSTRUMENT_TYPE_BOND)
13 | )
14 | print("BONDS")
15 | for bond in r.assets:
16 | print(bond)
17 | r = client.instruments.get_assets(
18 | request=AssetsRequest(
19 | instrument_status=InstrumentStatus.INSTRUMENT_STATUS_BASE
20 | )
21 | )
22 | print("BASE ASSETS")
23 | for bond in r.assets:
24 | print(bond)
25 |
26 |
27 | if __name__ == "__main__":
28 | main()
29 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/moving_average/strategy_state.py:
--------------------------------------------------------------------------------
1 | class MovingAverageStrategyState:
2 | def __init__(self):
3 | self._long_open: bool = False
4 | self._short_open: bool = False
5 | self._position: int = 0
6 |
7 | @property
8 | def long_open(self) -> bool:
9 | return self._long_open
10 |
11 | @long_open.setter
12 | def long_open(self, value: bool) -> None:
13 | self._long_open = value
14 |
15 | @property
16 | def short_open(self) -> bool:
17 | return self._short_open
18 |
19 | @short_open.setter
20 | def short_open(self, value: bool) -> None:
21 | self._short_open = value
22 |
23 | @property
24 | def position(self) -> int:
25 | return self._position
26 |
27 | @position.setter
28 | def position(self, value: int) -> None:
29 | self._position = value
30 |
--------------------------------------------------------------------------------
/examples/instruments/async_get_bond_events.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient, InstrumentType
5 | from tinkoff.invest.schemas import EventType, GetBondEventsRequest
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | bond = (
13 | await client.instruments.find_instrument(
14 | query="Тинькофф Банк выпуск 1",
15 | instrument_kind=InstrumentType.INSTRUMENT_TYPE_BOND,
16 | )
17 | ).instruments[0]
18 |
19 | request = GetBondEventsRequest(
20 | instrument_id=bond.uid,
21 | type=EventType.EVENT_TYPE_CALL,
22 | )
23 | print(await client.instruments.get_bond_events(request=request))
24 |
25 |
26 | if __name__ == "__main__":
27 | asyncio.run(main())
28 |
--------------------------------------------------------------------------------
/examples/instruments/get_forecast_by.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import timedelta
3 |
4 | from tinkoff.invest import Client
5 | from tinkoff.invest.schemas import (
6 | GetAssetReportsRequest,
7 | GetConsensusForecastsRequest,
8 | GetForecastRequest,
9 | InstrumentIdType,
10 | Page,
11 | )
12 | from tinkoff.invest.utils import now
13 |
14 | TOKEN = os.environ["INVEST_TOKEN"]
15 |
16 |
17 | def main():
18 | with Client(TOKEN) as client:
19 | instrument = client.instruments.find_instrument(
20 | query="Сбер Банк - привилегированные акции"
21 | ).instruments[0]
22 | request = GetForecastRequest(instrument_id=instrument.uid)
23 | response = client.instruments.get_forecast_by(request=request)
24 | print(instrument.name, response.consensus.recommendation.name)
25 |
26 |
27 | if __name__ == "__main__":
28 | main()
29 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/aio/grpc_interceptor.py:
--------------------------------------------------------------------------------
1 | import grpc
2 |
3 | from tinkoff.invest.retrying.aio.retry_manager import AsyncRetryManager
4 |
5 |
6 | class AsyncRetryClientInterceptor(grpc.aio.UnaryUnaryClientInterceptor):
7 | def __init__(
8 | self, retry_manager: AsyncRetryManager
9 | ): # pylint: disable=super-init-not-called
10 | self._retry_manager = retry_manager
11 |
12 | async def _intercept_with_retry(self, continuation, client_call_details, request):
13 | async def call():
14 | return await continuation(client_call_details, request)
15 |
16 | return await self._retry_manager.call_with_retries(call=call)
17 |
18 | async def intercept_unary_unary(self, continuation, client_call_details, request):
19 | return await self._intercept_with_retry(
20 | continuation, client_call_details, request
21 | )
22 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/sync/client.py:
--------------------------------------------------------------------------------
1 | from tinkoff.invest import Client
2 | from tinkoff.invest.retrying.settings_protocol import RetryClientSettingsProtocol
3 | from tinkoff.invest.retrying.sync.grpc_interceptor import RetryClientInterceptor
4 | from tinkoff.invest.retrying.sync.retry_manager import RetryManager
5 |
6 |
7 | class RetryingClient(Client):
8 | def __init__(
9 | self,
10 | token: str,
11 | settings: RetryClientSettingsProtocol,
12 | **kwargs,
13 | ):
14 | self._retry_manager = RetryManager(settings=settings)
15 | self._retry_interceptor = RetryClientInterceptor(
16 | retry_manager=self._retry_manager
17 | )
18 | interceptors = kwargs.get("interceptors", [])
19 | interceptors.append(self._retry_interceptor)
20 | kwargs["interceptors"] = interceptors
21 | super().__init__(token, **kwargs)
22 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/strategy_supervisor.py:
--------------------------------------------------------------------------------
1 | import abc
2 | from typing import Iterable, Protocol, Type
3 |
4 | from tinkoff.invest.strategies.base.event import StrategyEvent
5 |
6 |
7 | class IStrategySupervisor(Protocol):
8 | def notify(self, event: StrategyEvent) -> None:
9 | pass
10 |
11 | def get_events(self) -> Iterable[StrategyEvent]:
12 | pass
13 |
14 | def get_events_of_type(self, cls: Type[StrategyEvent]) -> Iterable[StrategyEvent]:
15 | pass
16 |
17 |
18 | class StrategySupervisor(abc.ABC, IStrategySupervisor):
19 | @abc.abstractmethod
20 | def notify(self, event: StrategyEvent) -> None:
21 | pass
22 |
23 | @abc.abstractmethod
24 | def get_events(self) -> Iterable[StrategyEvent]:
25 | pass
26 |
27 | @abc.abstractmethod
28 | def get_events_of_type(self, cls: Type[StrategyEvent]) -> Iterable[StrategyEvent]:
29 | pass
30 |
--------------------------------------------------------------------------------
/scripts/update_issue_templates.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import yaml
4 |
5 |
6 | def add_version(version: str, file: str) -> None:
7 | with open(file, "r", encoding="utf-8") as f:
8 | data = yaml.safe_load(f)
9 | for field in data["body"]:
10 | if field.get("id", "") == "package-version":
11 | field["attributes"]["options"] = [
12 | version,
13 | *field["attributes"]["options"],
14 | ]
15 | with open(file, "w+", encoding="utf-8") as f:
16 | yaml.dump(
17 | data, f, default_flow_style=False, sort_keys=False, allow_unicode=True
18 | )
19 |
20 |
21 | def main() -> None:
22 | version = sys.argv[1]
23 | add_version(version, ".github/ISSUE_TEMPLATE/bug_report.yaml")
24 | add_version(version, ".github/ISSUE_TEMPLATE/issue.yaml")
25 |
26 |
27 | if __name__ == "__main__":
28 | main()
29 |
--------------------------------------------------------------------------------
/examples/async_get_signals.py:
--------------------------------------------------------------------------------
1 | """Example - How to get Signals"""
2 | import asyncio
3 | import datetime
4 | import os
5 |
6 | from tinkoff.invest import AsyncClient
7 | from tinkoff.invest.schemas import GetSignalsRequest, SignalState
8 |
9 | TOKEN = os.environ["INVEST_TOKEN"]
10 |
11 |
12 | async def main():
13 | async with AsyncClient(TOKEN) as client:
14 | request = GetSignalsRequest()
15 | request.instrument_uid = "e6123145-9665-43e0-8413-cd61b8aa9b13" # Сбербанк
16 | request.active = SignalState.SIGNAL_STATE_ALL # все сигналы
17 | request.from_ = datetime.datetime.now() - datetime.timedelta(
18 | weeks=4
19 | ) # сигналы, созданные не больше чем 4 недели назад
20 | r = await client.signals.get_signals(request=request)
21 | for signal in r.signals:
22 | print(signal)
23 |
24 |
25 | if __name__ == "__main__":
26 | asyncio.run(main())
27 |
--------------------------------------------------------------------------------
/tests/caches/test_ttl_cache.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from cachetools import TTLCache as StandardTTLCache
4 | from pytest_freezegun import freeze_time
5 |
6 | from tinkoff.invest.caching.overrides import TTLCache as OverridenTTLCache
7 |
8 |
9 | class TestTTLCache:
10 | def _assert_ttl_cache(self, ttl_cache_class, expires):
11 | with freeze_time() as frozen_datetime:
12 | ttl = ttl_cache_class(
13 | maxsize=10,
14 | ttl=1,
15 | )
16 | ttl.update({"1": 1})
17 |
18 | assert ttl.keys()
19 | frozen_datetime.tick(timedelta(seconds=10000))
20 | assert not ttl.keys() == expires
21 |
22 | def test_overriden_cache(self):
23 | self._assert_ttl_cache(OverridenTTLCache, expires=True)
24 |
25 | def test_standard_cache(self):
26 | self._assert_ttl_cache(StandardTTLCache, expires=False)
27 |
--------------------------------------------------------------------------------
/examples/instruments/async_get_asset_reports.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | from datetime import timedelta
4 |
5 | from tinkoff.invest import AsyncClient
6 | from tinkoff.invest.schemas import GetAssetReportsRequest
7 | from tinkoff.invest.utils import now
8 |
9 | TOKEN = os.environ["INVEST_TOKEN"]
10 |
11 |
12 | async def main():
13 | async with AsyncClient(TOKEN) as client:
14 | instruments = await client.instruments.find_instrument(
15 | query="Тинькофф Квадратные метры"
16 | )
17 | instrument = instruments.instruments[0]
18 | print(instrument.name)
19 | request = GetAssetReportsRequest(
20 | instrument_id=instrument.uid,
21 | from_=now() - timedelta(days=7),
22 | to=now(),
23 | )
24 | print(await client.instruments.get_asset_reports(request=request))
25 |
26 |
27 | if __name__ == "__main__":
28 | asyncio.run(main())
29 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/aio/client.py:
--------------------------------------------------------------------------------
1 | from tinkoff.invest import AsyncClient
2 | from tinkoff.invest.retrying.aio.grpc_interceptor import AsyncRetryClientInterceptor
3 | from tinkoff.invest.retrying.aio.retry_manager import AsyncRetryManager
4 | from tinkoff.invest.retrying.settings_protocol import RetryClientSettingsProtocol
5 |
6 |
7 | class AsyncRetryingClient(AsyncClient):
8 | def __init__(
9 | self,
10 | token: str,
11 | settings: RetryClientSettingsProtocol,
12 | **kwargs,
13 | ):
14 | self._retry_manager = AsyncRetryManager(settings=settings)
15 | self._retry_interceptor = AsyncRetryClientInterceptor(
16 | retry_manager=self._retry_manager
17 | )
18 | interceptors = kwargs.get("interceptors", [])
19 | interceptors.append(self._retry_interceptor)
20 | kwargs["interceptors"] = interceptors
21 | super().__init__(token, **kwargs)
22 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/base_retry_manager.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from tinkoff.invest.retrying.settings_protocol import RetryClientSettingsProtocol
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 |
8 | class BaseRetryManager:
9 | def __init__(self, settings: RetryClientSettingsProtocol):
10 | self._settings = settings
11 |
12 | def get_initial_retries(self):
13 | retries_left = self._settings.max_retry_attempt
14 | if not self._settings.use_retry:
15 | retries_left = 0
16 | logger.debug("Retrying disabled")
17 | retries_left += 1
18 | return retries_left
19 |
20 | @staticmethod
21 | def extract_seconds_to_sleep(metadata) -> int:
22 | logger.debug("Received metadata %s", metadata)
23 | seconds_to_sleep = metadata.ratelimit_reset
24 | logger.debug("Sleeping for %s seconds", seconds_to_sleep)
25 | return seconds_to_sleep
26 |
--------------------------------------------------------------------------------
/examples/post_order_async.py:
--------------------------------------------------------------------------------
1 | """Example - How to get Post Order"""
2 |
3 | import os
4 | from uuid import uuid4
5 |
6 | from tinkoff.invest import Client, OrderDirection, OrderType
7 | from tinkoff.invest.schemas import PostOrderAsyncRequest
8 |
9 | TOKEN = os.environ["INVEST_TOKEN"]
10 |
11 |
12 | def main():
13 | with Client(TOKEN) as client:
14 | accounts = client.users.get_accounts()
15 | account_id = accounts.accounts[0].id
16 |
17 | request = PostOrderAsyncRequest(
18 | order_type=OrderType.ORDER_TYPE_MARKET,
19 | direction=OrderDirection.ORDER_DIRECTION_BUY,
20 | instrument_id="BBG004730ZJ9",
21 | quantity=1,
22 | account_id=account_id,
23 | order_id=str(uuid4()),
24 | )
25 | response = client.orders.post_order_async(request=request)
26 | print(response)
27 |
28 |
29 | if __name__ == "__main__":
30 | main()
31 |
--------------------------------------------------------------------------------
/examples/get_operations_by_cursor.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pprint import pprint
3 |
4 | from tinkoff.invest import Client, GetOperationsByCursorRequest
5 |
6 | token = os.environ["INVEST_TOKEN"]
7 |
8 |
9 | with Client(token) as client:
10 | accounts = client.users.get_accounts()
11 | account_id = accounts.accounts[0].id
12 |
13 | def get_request(cursor=""):
14 | return GetOperationsByCursorRequest(
15 | account_id=account_id,
16 | instrument_id="BBG004730N88",
17 | cursor=cursor,
18 | limit=1,
19 | )
20 |
21 | operations = client.operations.get_operations_by_cursor(get_request())
22 | print(operations)
23 | depth = 10
24 | while operations.has_next and depth > 0:
25 | request = get_request(cursor=operations.next_cursor)
26 | operations = client.operations.get_operations_by_cursor(request)
27 | pprint(operations)
28 | depth -= 1
29 |
--------------------------------------------------------------------------------
/examples/async_post_order_async.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | from uuid import uuid4
4 |
5 | from tinkoff.invest import AsyncClient
6 | from tinkoff.invest.schemas import OrderDirection, OrderType, PostOrderAsyncRequest
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 |
11 | async def main():
12 | async with AsyncClient(TOKEN) as client:
13 | accounts = await client.users.get_accounts()
14 | account_id = accounts.accounts[0].id
15 | request = PostOrderAsyncRequest(
16 | order_type=OrderType.ORDER_TYPE_MARKET,
17 | direction=OrderDirection.ORDER_DIRECTION_BUY,
18 | instrument_id="BBG004730ZJ9",
19 | quantity=1,
20 | account_id=account_id,
21 | order_id=str(uuid4()),
22 | )
23 | response = await client.orders.post_order_async(request=request)
24 | print(response)
25 |
26 |
27 | if __name__ == "__main__":
28 | asyncio.run(main())
29 |
--------------------------------------------------------------------------------
/.github/workflows/bumpversion.yml:
--------------------------------------------------------------------------------
1 | name: Bump version
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - "tinkoff/**"
8 | - "!tinkoff/invest/__init__.py"
9 | - "!tinkoff/invest/constants.py"
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 0
19 | token: ${{ secrets.BOT_ACCESS_TOKEN }}
20 |
21 | - name: Git user
22 | run: |
23 | git config --local user.name 'github-actions[bot]'
24 | git config --local user.email 'github-actions[bot]@users.noreply.github.com'
25 |
26 | - name: Install python dependencies
27 | run: make install-poetry install-bump
28 |
29 | - name: Bump version
30 | run: make bump-version v=$(make next-version)
31 |
32 | - name: Push
33 | run: |
34 | git push
35 | git push --tags
36 |
--------------------------------------------------------------------------------
/examples/max_lots.py:
--------------------------------------------------------------------------------
1 | """Example - How to get available limits."""
2 |
3 | import logging
4 | import os
5 |
6 | from tinkoff.invest import Client, GetMaxLotsRequest
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 | logger = logging.getLogger(__name__)
11 | logging.basicConfig(level=logging.INFO)
12 |
13 |
14 | INSTRUMENT_ID = "TCS00A105GE2"
15 |
16 |
17 | def main():
18 | logger.info("Getting Max Lots")
19 | with Client(TOKEN) as client:
20 | response = client.users.get_accounts()
21 | account, *_ = response.accounts
22 | account_id = account.id
23 |
24 | logger.info(
25 | "Calculating available order amount for instrument=%s and market price",
26 | INSTRUMENT_ID,
27 | )
28 | get_max_lots = client.orders.get_max_lots(
29 | GetMaxLotsRequest(account_id=account_id, instrument_id=INSTRUMENT_ID)
30 | )
31 |
32 | print(get_max_lots)
33 |
34 |
35 | if __name__ == "__main__":
36 | main()
37 |
--------------------------------------------------------------------------------
/examples/async_get_market_values.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import GetMarketValuesRequest, MarketValueType
6 |
7 | TOKEN = os.environ["INVEST_TOKEN"]
8 |
9 |
10 | async def main():
11 | async with AsyncClient(TOKEN) as client:
12 | market_values = await client.market_data.get_market_values(
13 | request=GetMarketValuesRequest(
14 | instrument_id=["BBG004730N88", "64c0da45-4c90-41d4-b053-0c66c7a8ddcd"],
15 | values=[
16 | MarketValueType.INSTRUMENT_VALUE_LAST_PRICE,
17 | MarketValueType.INSTRUMENT_VALUE_CLOSE_PRICE,
18 | ],
19 | )
20 | )
21 | for instrument in market_values.instruments:
22 | print(instrument.instrument_uid)
23 | for value in instrument.values:
24 | print(value)
25 |
26 |
27 | if __name__ == "__main__":
28 | asyncio.run(main())
29 |
--------------------------------------------------------------------------------
/examples/async_retrying_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | import os
4 | from datetime import timedelta
5 |
6 | from tinkoff.invest import CandleInterval
7 | from tinkoff.invest.retrying.aio.client import AsyncRetryingClient
8 | from tinkoff.invest.retrying.settings import RetryClientSettings
9 | from tinkoff.invest.utils import now
10 |
11 | logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG)
12 |
13 | TOKEN = os.environ["INVEST_TOKEN"]
14 |
15 | retry_settings = RetryClientSettings(use_retry=True, max_retry_attempt=2)
16 |
17 |
18 | async def main():
19 | async with AsyncRetryingClient(TOKEN, settings=retry_settings) as client:
20 | async for candle in client.get_all_candles(
21 | figi="BBG000B9XRY4",
22 | from_=now() - timedelta(days=301),
23 | interval=CandleInterval.CANDLE_INTERVAL_1_MIN,
24 | ):
25 | print(candle)
26 |
27 |
28 | if __name__ == "__main__":
29 | asyncio.run(main())
30 |
--------------------------------------------------------------------------------
/examples/post_order.py:
--------------------------------------------------------------------------------
1 | """Example - How to Post order"""
2 |
3 | import os
4 | from uuid import uuid4
5 |
6 | from tinkoff.invest import Client, OrderDirection, OrderType
7 | from tinkoff.invest.sandbox.client import SandboxClient
8 |
9 | TOKEN = os.environ["INVEST_TOKEN"]
10 |
11 | """
12 | Примеры дешевых акций:
13 | BBG001M2SC01 84.120000000р
14 | BBG000K3STR7 134.900000000р
15 | BBG00F9XX7H4 142.000000000р
16 | """
17 |
18 |
19 | def main():
20 | with Client(TOKEN) as client:
21 | accounts = client.users.get_accounts()
22 | account_id = accounts.accounts[0].id
23 |
24 | response = client.orders.post_order(
25 | order_type=OrderType.ORDER_TYPE_MARKET,
26 | direction=OrderDirection.ORDER_DIRECTION_BUY,
27 | instrument_id="BBG004730ZJ9",
28 | quantity=1,
29 | account_id=account_id,
30 | order_id=str(uuid4()),
31 | )
32 | print(response)
33 |
34 |
35 | if __name__ == "__main__":
36 | main()
37 |
--------------------------------------------------------------------------------
/examples/easy_stream_client.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import (
4 | CandleInstrument,
5 | Client,
6 | InfoInstrument,
7 | SubscriptionInterval,
8 | )
9 | from tinkoff.invest.services import MarketDataStreamManager
10 |
11 | TOKEN = os.environ["INVEST_TOKEN"]
12 |
13 |
14 | def main():
15 | with Client(TOKEN) as client:
16 | market_data_stream: MarketDataStreamManager = client.create_market_data_stream()
17 | market_data_stream.candles.waiting_close().subscribe(
18 | [
19 | CandleInstrument(
20 | figi="BBG004730N88",
21 | interval=SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE,
22 | )
23 | ]
24 | )
25 | for marketdata in market_data_stream:
26 | print(marketdata)
27 | market_data_stream.info.subscribe([InfoInstrument(figi="BBG004730N88")])
28 | if marketdata.subscribe_info_response:
29 | market_data_stream.stop()
30 |
31 |
32 | if __name__ == "__main__":
33 | main()
34 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/signal.py:
--------------------------------------------------------------------------------
1 | import enum
2 | from dataclasses import dataclass, field
3 |
4 |
5 | class SignalDirection(enum.Enum):
6 | LONG = "LONG"
7 | SHORT = "SHORT"
8 |
9 |
10 | @dataclass
11 | class Signal:
12 | pass
13 |
14 |
15 | @dataclass
16 | class OrderSignal(Signal):
17 | lots: int
18 | direction: SignalDirection
19 |
20 |
21 | @dataclass
22 | class CloseSignal(OrderSignal):
23 | pass
24 |
25 |
26 | @dataclass
27 | class OpenSignal(OrderSignal):
28 | pass
29 |
30 |
31 | @dataclass
32 | class OpenLongMarketOrder(OpenSignal):
33 | direction: SignalDirection = field(default=SignalDirection.LONG)
34 |
35 |
36 | @dataclass
37 | class CloseLongMarketOrder(CloseSignal):
38 | direction: SignalDirection = field(default=SignalDirection.LONG)
39 |
40 |
41 | @dataclass
42 | class OpenShortMarketOrder(OpenSignal):
43 | direction: SignalDirection = field(default=SignalDirection.SHORT)
44 |
45 |
46 | @dataclass
47 | class CloseShortMarketOrder(CloseSignal):
48 | direction: SignalDirection = field(default=SignalDirection.SHORT)
49 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/moving_average/supervisor.py:
--------------------------------------------------------------------------------
1 | from itertools import chain
2 | from typing import Dict, Iterable, List, Type, cast
3 |
4 | from tinkoff.invest.strategies.base.errors import EventsWereNotSupervised
5 | from tinkoff.invest.strategies.base.event import StrategyEvent
6 | from tinkoff.invest.strategies.base.strategy_supervisor import StrategySupervisor
7 |
8 |
9 | class MovingAverageStrategySupervisor(StrategySupervisor):
10 | def __init__(self):
11 | self._events: Dict[Type[StrategyEvent], List[StrategyEvent]] = {}
12 |
13 | def notify(self, event: StrategyEvent) -> None:
14 | if type(event) not in self._events:
15 | self._events[type(event)] = []
16 | self._events[type(event)].append(event)
17 |
18 | def get_events(self) -> Iterable[StrategyEvent]:
19 | return cast(Iterable[StrategyEvent], chain(*self._events.values()))
20 |
21 | def get_events_of_type(self, cls: Type[StrategyEvent]) -> List[StrategyEvent]:
22 | if cls in self._events:
23 | return self._events[cls]
24 | raise EventsWereNotSupervised()
25 |
--------------------------------------------------------------------------------
/examples/instruments/get_insider_deals.py:
--------------------------------------------------------------------------------
1 | """Example - How to get list of insider deals.
2 | Request data in loop with batches of 10 records.
3 | """
4 | import logging
5 | import os
6 |
7 | from tinkoff.invest import Client
8 | from tinkoff.invest.schemas import GetInsiderDealsRequest
9 |
10 | TOKEN = os.environ["INVEST_TOKEN"]
11 |
12 | logger = logging.getLogger(__name__)
13 | logging.basicConfig(level=logging.INFO)
14 |
15 |
16 | def main():
17 | with Client(TOKEN) as client:
18 | deals = []
19 |
20 | next_cursor = None
21 | while True:
22 | response = client.instruments.get_insider_deals(
23 | request=GetInsiderDealsRequest(
24 | instrument_id="BBG004730N88", limit=10, next_cursor=next_cursor
25 | )
26 | )
27 | deals.extend(response.insider_deals)
28 | next_cursor = response.next_cursor
29 | if not next_cursor:
30 | break
31 | print("Insider deals:")
32 | for deal in deals:
33 | print(deal)
34 |
35 |
36 | if __name__ == "__main__":
37 | main()
38 |
--------------------------------------------------------------------------------
/examples/download_all_candles.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from datetime import timedelta
4 | from pathlib import Path
5 |
6 | from tinkoff.invest import CandleInterval, Client
7 | from tinkoff.invest.caching.market_data_cache.cache import MarketDataCache
8 | from tinkoff.invest.caching.market_data_cache.cache_settings import (
9 | MarketDataCacheSettings,
10 | )
11 | from tinkoff.invest.utils import now
12 |
13 | TOKEN = os.environ["INVEST_TOKEN"]
14 | logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG)
15 |
16 |
17 | def main():
18 | with Client(TOKEN) as client:
19 | settings = MarketDataCacheSettings(base_cache_dir=Path("market_data_cache"))
20 | market_data_cache = MarketDataCache(settings=settings, services=client)
21 | for candle in market_data_cache.get_all_candles(
22 | figi="BBG004730N88",
23 | from_=now() - timedelta(days=1),
24 | interval=CandleInterval.CANDLE_INTERVAL_HOUR,
25 | ):
26 | print(candle.time, candle.is_complete)
27 |
28 | return 0
29 |
30 |
31 | if __name__ == "__main__":
32 | main()
33 |
--------------------------------------------------------------------------------
/examples/sandbox/sandbox_get_stop_orders.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest.sandbox.client import SandboxClient
4 | from tinkoff.invest.schemas import (
5 | GetStopOrdersRequest,
6 | GetStopOrdersResponse,
7 | StopOrderStatusOption,
8 | )
9 |
10 |
11 | def main():
12 | token = os.environ["INVEST_TOKEN"]
13 |
14 | with SandboxClient(token) as client:
15 | accounts = client.users.get_accounts()
16 | account_id = accounts.accounts[0].id
17 | response = get_stop_orders(
18 | client, account_id, StopOrderStatusOption.STOP_ORDER_STATUS_ACTIVE
19 | )
20 | if len(response.stop_orders) > 0:
21 | print(response.stop_orders)
22 | else:
23 | print("Активных отложенных заявок не найдено")
24 |
25 |
26 | def get_stop_orders(sandbox_service, account_id, status) -> GetStopOrdersResponse:
27 | return sandbox_service.sandbox.get_sandbox_stop_orders(
28 | request=GetStopOrdersRequest(
29 | account_id=account_id,
30 | status=status,
31 | )
32 | )
33 |
34 |
35 | if __name__ == "__main__":
36 | main()
37 |
--------------------------------------------------------------------------------
/tinkoff/invest/exceptions.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from grpc import StatusCode
4 |
5 | __all__ = (
6 | "InvestError",
7 | "RequestError",
8 | "AioRequestError",
9 | )
10 |
11 |
12 | class InvestError(Exception):
13 | pass
14 |
15 |
16 | class RequestError(InvestError):
17 | def __init__( # pylint:disable=super-init-not-called
18 | self, code: StatusCode, details: str, metadata: Any
19 | ) -> None:
20 | self.code = code
21 | self.details = details
22 | self.metadata = metadata
23 |
24 |
25 | class UnauthenticatedError(RequestError):
26 | pass
27 |
28 |
29 | class AioRequestError(InvestError):
30 | def __init__( # pylint:disable=super-init-not-called
31 | self, code: StatusCode, details: str, metadata: Any
32 | ) -> None:
33 | self.code = code
34 | self.details = details
35 | self.metadata = metadata
36 |
37 |
38 | class AioUnauthenticatedError(AioRequestError):
39 | pass
40 |
41 |
42 | class MarketDataStreamError(InvestError):
43 | pass
44 |
45 |
46 | class IsNotSubscribedError(MarketDataStreamError):
47 | pass
48 |
--------------------------------------------------------------------------------
/examples/users/currency_transfer.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from _decimal import Decimal
4 |
5 | from tinkoff.invest import AccountType, Client
6 | from tinkoff.invest.schemas import CurrencyTransferRequest
7 | from tinkoff.invest.utils import decimal_to_money
8 |
9 |
10 | def main():
11 | token = os.environ["INVEST_TOKEN"]
12 |
13 | with Client(token) as client:
14 | accounts = [
15 | i
16 | for i in client.users.get_accounts().accounts
17 | if i.type == AccountType.ACCOUNT_TYPE_TINKOFF
18 | ]
19 |
20 | if len(accounts) < 2:
21 | print("Недостаточно счетов для демонстрации")
22 | return
23 |
24 | from_account_id = accounts[0].id
25 | to_account_id = accounts[1].id
26 |
27 | client.users.currency_transfer(
28 | CurrencyTransferRequest(
29 | from_account_id=from_account_id,
30 | to_account_id=to_account_id,
31 | amount=decimal_to_money(Decimal(1), "rub"),
32 | )
33 | )
34 | print("Перевод выполнен")
35 |
36 |
37 | if __name__ == "__main__":
38 | main()
39 |
--------------------------------------------------------------------------------
/examples/async_get_insider_deals.py:
--------------------------------------------------------------------------------
1 | """Example - How to get list of insider deals.
2 | Request data in loop with batches of 10 records.
3 | """
4 | import asyncio
5 | import logging
6 | import os
7 |
8 | from tinkoff.invest import AsyncClient
9 | from tinkoff.invest.schemas import GetInsiderDealsRequest
10 |
11 | TOKEN = os.environ["INVEST_TOKEN"]
12 |
13 | logger = logging.getLogger(__name__)
14 | logging.basicConfig(level=logging.INFO)
15 |
16 |
17 | async def main():
18 | async with AsyncClient(TOKEN) as client:
19 | deals = []
20 |
21 | next_cursor = None
22 | while True:
23 | response = await client.instruments.get_insider_deals(
24 | request=GetInsiderDealsRequest(
25 | instrument_id="BBG004730N88", limit=10, next_cursor=next_cursor
26 | )
27 | )
28 | deals.extend(response.insider_deals)
29 | if not next_cursor:
30 | break
31 | next_cursor = response.next_cursor
32 | print("Insider deals:")
33 | for deal in deals:
34 | print(deal)
35 |
36 |
37 | if __name__ == "__main__":
38 | asyncio.run(main())
39 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/sync/retry_manager.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 | from typing import Any
4 |
5 | from tinkoff.invest.logging import get_metadata_from_call
6 | from tinkoff.invest.retrying.base_retry_manager import BaseRetryManager
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | class RetryManager(BaseRetryManager):
12 | def call_with_retries(self, call: Any):
13 | retries_left = self.get_initial_retries()
14 | while retries_left > 0:
15 | logger.debug("Trying to call")
16 | result = call()
17 | logger.debug("Call succeeded")
18 | exception = result.exception()
19 | if not exception:
20 | return result
21 | retries_left -= 1
22 | logger.debug("Retries left = %s", retries_left)
23 | metadata = get_metadata_from_call(exception)
24 | seconds_to_sleep = self.extract_seconds_to_sleep(metadata)
25 | self._sleep(seconds_to_sleep)
26 |
27 | logger.debug("RetryManager exhausted, no retries left")
28 | return result
29 |
30 | def _sleep(self, seconds_to_sleep):
31 | time.sleep(seconds_to_sleep)
32 |
--------------------------------------------------------------------------------
/examples/users/async_currency_transfer.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from _decimal import Decimal
5 |
6 | from tinkoff.invest import AccountType, AsyncClient
7 | from tinkoff.invest.schemas import CurrencyTransferRequest
8 | from tinkoff.invest.utils import decimal_to_money
9 |
10 |
11 | async def main():
12 | token = os.environ["INVEST_TOKEN"]
13 |
14 | async with AsyncClient(token) as client:
15 | accounts = [
16 | i
17 | for i in (await client.users.get_accounts()).accounts
18 | if i.type == AccountType.ACCOUNT_TYPE_TINKOFF
19 | ]
20 |
21 | if len(accounts) < 2:
22 | print("Недостаточно счетов для демонстрации")
23 | return
24 |
25 | from_account_id = accounts[0].id
26 | to_account_id = accounts[1].id
27 |
28 | await client.users.currency_transfer(
29 | CurrencyTransferRequest(
30 | from_account_id=from_account_id,
31 | to_account_id=to_account_id,
32 | amount=decimal_to_money(Decimal(1), "rub"),
33 | )
34 | )
35 | print("Перевод выполнен")
36 |
37 |
38 | if __name__ == "__main__":
39 | asyncio.run(main())
40 |
--------------------------------------------------------------------------------
/examples/async_stream_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import (
5 | AsyncClient,
6 | CandleInstrument,
7 | MarketDataRequest,
8 | SubscribeCandlesRequest,
9 | SubscriptionAction,
10 | SubscriptionInterval,
11 | )
12 |
13 | TOKEN = os.environ["INVEST_TOKEN"]
14 |
15 |
16 | async def main():
17 | async def request_iterator():
18 | yield MarketDataRequest(
19 | subscribe_candles_request=SubscribeCandlesRequest(
20 | subscription_action=SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE,
21 | instruments=[
22 | CandleInstrument(
23 | figi="BBG004730N88",
24 | interval=SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE,
25 | )
26 | ],
27 | )
28 | )
29 | while True:
30 | await asyncio.sleep(1)
31 |
32 | async with AsyncClient(TOKEN) as client:
33 | async for marketdata in client.market_data_stream.market_data_stream(
34 | request_iterator()
35 | ):
36 | print(marketdata)
37 |
38 |
39 | if __name__ == "__main__":
40 | asyncio.run(main())
41 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Tinkoff Invest Python SDK
2 | site_url: https://RussianInvestments.github.io/invest-python/
3 | site_description: 'Python SDK, gRPC client, bot examples'
4 |
5 | repo_name: 'RussianInvestments/invest-python'
6 | repo_url: 'https://github.com/RussianInvestments/invest-python'
7 | edit_uri: "edit/main/docs/"
8 |
9 | copyright: 'Copyright © 2023 Tinkoff'
10 |
11 | use_directory_urls: true
12 | nav:
13 | - 'Главная': 'README.md'
14 | - 'API Reference':
15 | - Clients: api/clients.md
16 | - 'Примеры': 'examples.md'
17 | - 'Готовые работы': 'robots.md'
18 | - 'Список изменений': 'CHANGELOG.md'
19 | - 'Участие в проекте': 'CONTRIBUTING.md'
20 |
21 | theme:
22 | name: material
23 | language: ru
24 | palette:
25 | primary: black
26 | accent: yellow
27 |
28 | plugins:
29 | - include-markdown
30 | - termynal: {}
31 | - search:
32 | lang: ru
33 | - mkdocstrings:
34 | default_handler: python
35 | handlers:
36 | python:
37 | rendering:
38 | show_source: false
39 |
40 | extra_css:
41 | - custom.css
42 |
43 | markdown_extensions:
44 | - admonition
45 | - codehilite
46 | - pymdownx.superfences
47 | - tables
48 | - pymdownx.tasklist:
49 | custom_checkbox: true
50 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: CI Tests/Lints
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build-ubuntu:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - uses: actions/setup-python@v4
17 | with:
18 | python-version: '3.8'
19 |
20 | - name: Install python dependencies
21 | run: make install-poetry install
22 |
23 | - name: Run linters
24 | run: make lint
25 |
26 | - name: Test docs
27 | run: make docs
28 |
29 | - name: Run test
30 | run: make test
31 | env:
32 | INVEST_SANDBOX_TOKEN: ${{ secrets.INVEST_SANDBOX_TOKEN }}
33 | INVEST_TOKEN: ${{ secrets.INVEST_TOKEN }}
34 |
35 | build-windows:
36 | runs-on: windows-latest
37 | steps:
38 | - uses: actions/checkout@v2
39 |
40 | - name: Install make
41 | run: choco install make
42 |
43 | - name: Install python dependencies
44 | run: make install-poetry install
45 |
46 | - name: Run test
47 | run: make test
48 | env:
49 | INVEST_SANDBOX_TOKEN: ${{ secrets.INVEST_SANDBOX_TOKEN }}
50 |
--------------------------------------------------------------------------------
/examples/get_orders.py:
--------------------------------------------------------------------------------
1 | """Example - How to get list of orders for 1 last hour (maximum requesting period)."""
2 | import datetime
3 | import logging
4 | import os
5 |
6 | from tinkoff.invest import Client
7 | from tinkoff.invest.schemas import OrderExecutionReportStatus
8 |
9 | TOKEN = os.environ["INVEST_TOKEN"]
10 |
11 | logger = logging.getLogger(__name__)
12 | logging.basicConfig(level=logging.INFO)
13 |
14 |
15 | def main():
16 | with Client(TOKEN) as client:
17 | response = client.users.get_accounts()
18 | account, *_ = response.accounts
19 | account_id = account.id
20 |
21 | now = datetime.datetime.now()
22 | orders = client.orders.get_orders(
23 | account_id=account_id,
24 | from_=now - datetime.timedelta(hours=1),
25 | to=now,
26 | # filter only executed or partially executed orders
27 | execution_status=[
28 | OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_FILL,
29 | OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_PARTIALLYFILL,
30 | ],
31 | )
32 | print("Orders list:")
33 | for order in orders.orders:
34 | print(order)
35 |
36 |
37 | if __name__ == "__main__":
38 | main()
39 |
--------------------------------------------------------------------------------
/examples/async_get_orders.py:
--------------------------------------------------------------------------------
1 | """Example - How to get list of orders for 1 last hour (maximum requesting period)."""
2 | import asyncio
3 | import datetime
4 | import logging
5 | import os
6 |
7 | from tinkoff.invest import AsyncClient
8 | from tinkoff.invest.schemas import OrderExecutionReportStatus
9 |
10 | TOKEN = os.environ["INVEST_TOKEN"]
11 |
12 | logger = logging.getLogger(__name__)
13 | logging.basicConfig(level=logging.INFO)
14 |
15 |
16 | async def main():
17 | async with AsyncClient(TOKEN) as client:
18 | response = await client.users.get_accounts()
19 | account, *_ = response.accounts
20 | account_id = account.id
21 |
22 | now = datetime.datetime.now()
23 | orders = await client.orders.get_orders(
24 | account_id=account_id,
25 | from_=now - datetime.timedelta(hours=1),
26 | to=now,
27 | # filter only executed or partially executed orders
28 | execution_status=[
29 | OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_FILL,
30 | OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_PARTIALLYFILL,
31 | ],
32 | )
33 | print("Orders list:")
34 | for order in orders.orders:
35 | print(order)
36 |
37 |
38 | if __name__ == "__main__":
39 | asyncio.run(main())
40 |
--------------------------------------------------------------------------------
/examples/stream_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 |
4 | from tinkoff.invest import (
5 | CandleInstrument,
6 | Client,
7 | MarketDataRequest,
8 | SubscribeCandlesRequest,
9 | SubscriptionAction,
10 | SubscriptionInterval,
11 | )
12 | from tinkoff.invest.schemas import CandleSource
13 |
14 | TOKEN = os.environ["INVEST_TOKEN"]
15 |
16 |
17 | def main():
18 | def request_iterator():
19 | yield MarketDataRequest(
20 | subscribe_candles_request=SubscribeCandlesRequest(
21 | waiting_close=True,
22 | subscription_action=SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE,
23 | candle_source_type=CandleSource.CANDLE_SOURCE_EXCHANGE,
24 | instruments=[
25 | CandleInstrument(
26 | figi="BBG004730N88",
27 | interval=SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE,
28 | )
29 | ],
30 | )
31 | )
32 | while True:
33 | time.sleep(1)
34 |
35 | with Client(TOKEN) as client:
36 | for marketdata in client.market_data_stream.market_data_stream(
37 | request_iterator()
38 | ):
39 | print(marketdata)
40 |
41 |
42 | if __name__ == "__main__":
43 | main()
44 |
--------------------------------------------------------------------------------
/tinkoff/invest/retrying/aio/retry_manager.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from typing import Any
4 |
5 | from grpc.aio import AioRpcError
6 |
7 | from tinkoff.invest.logging import get_metadata_from_aio_error
8 | from tinkoff.invest.retrying.base_retry_manager import BaseRetryManager
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class AsyncRetryManager(BaseRetryManager):
14 | async def call_with_retries(self, call: Any):
15 | retries_left = self.get_initial_retries()
16 | while retries_left > 0:
17 | logger.debug("Trying to call")
18 | response = await call()
19 | try:
20 | await response
21 | logger.debug("Call succeeded")
22 | return response
23 | except AioRpcError as exception:
24 | retries_left -= 1
25 | logger.debug("Retries left = %s", retries_left)
26 |
27 | metadata = get_metadata_from_aio_error(exception)
28 | seconds_to_sleep = self.extract_seconds_to_sleep(metadata)
29 | await self._sleep(seconds_to_sleep)
30 |
31 | logger.debug("RetryManager exhausted, no retries left")
32 | return response
33 |
34 | async def _sleep(self, seconds_to_sleep):
35 | await asyncio.sleep(seconds_to_sleep)
36 |
--------------------------------------------------------------------------------
/examples/get_tech_analysis.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import timedelta
3 | from decimal import Decimal
4 |
5 | from tinkoff.invest import Client
6 | from tinkoff.invest.schemas import (
7 | Deviation,
8 | GetTechAnalysisRequest,
9 | IndicatorInterval,
10 | IndicatorType,
11 | Smoothing,
12 | TypeOfPrice,
13 | )
14 | from tinkoff.invest.utils import decimal_to_quotation, now
15 |
16 | TOKEN = os.environ["INVEST_TOKEN"]
17 |
18 |
19 | def main():
20 | with Client(TOKEN) as client:
21 | request = GetTechAnalysisRequest(
22 | indicator_type=IndicatorType.INDICATOR_TYPE_RSI,
23 | instrument_uid="6542a064-6633-44ba-902f-710c97507522",
24 | from_=now() - timedelta(days=7),
25 | to=now(),
26 | interval=IndicatorInterval.INDICATOR_INTERVAL_4_HOUR,
27 | type_of_price=TypeOfPrice.TYPE_OF_PRICE_AVG,
28 | length=42,
29 | deviation=Deviation(
30 | deviation_multiplier=decimal_to_quotation(Decimal(1.0)),
31 | ),
32 | smoothing=Smoothing(fast_length=13, slow_length=7, signal_smoothing=3),
33 | )
34 | response = client.market_data.get_tech_analysis(request=request)
35 | for indicator in response.technical_indicators:
36 | print(indicator.signal)
37 |
38 |
39 | if __name__ == "__main__":
40 | main()
41 |
--------------------------------------------------------------------------------
/examples/sandbox/sandbox_cancel_stop_order.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest.sandbox.client import SandboxClient
4 | from tinkoff.invest.schemas import (
5 | CancelStopOrderRequest,
6 | CancelStopOrderResponse,
7 | GetStopOrdersRequest,
8 | StopOrderStatusOption,
9 | )
10 |
11 |
12 | def main():
13 | token = os.environ["INVEST_TOKEN"]
14 |
15 | with SandboxClient(token) as client:
16 | accounts = client.users.get_accounts()
17 | account_id = accounts.accounts[0].id
18 | stop_orders = client.sandbox.get_sandbox_stop_orders(
19 | request=GetStopOrdersRequest(
20 | account_id=account_id,
21 | status=StopOrderStatusOption.STOP_ORDER_STATUS_ACTIVE,
22 | )
23 | )
24 | stop_order_id = stop_orders.stop_orders[0].stop_order_id
25 | response = cancel_stop_order(client, account_id, stop_order_id)
26 | print(f"Отменена отложенная заявка id={stop_order_id}: {response}")
27 |
28 |
29 | def cancel_stop_order(
30 | sandbox_service, account_id, stop_order_id
31 | ) -> CancelStopOrderResponse:
32 | return sandbox_service.sandbox.cancel_sandbox_stop_order(
33 | request=CancelStopOrderRequest(
34 | account_id=account_id,
35 | stop_order_id=stop_order_id,
36 | )
37 | )
38 |
39 |
40 | if __name__ == "__main__":
41 | main()
42 |
--------------------------------------------------------------------------------
/tests/test_operations.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=redefined-outer-name,unused-variable
2 |
3 | from unittest import mock
4 |
5 | import pytest
6 |
7 | from tinkoff.invest.services import OperationsService
8 |
9 |
10 | @pytest.fixture()
11 | def operations_service():
12 | return mock.create_autospec(spec=OperationsService)
13 |
14 |
15 | def test_get_operations(operations_service):
16 | response = operations_service.get_operations( # noqa: F841
17 | account_id=mock.Mock(),
18 | from_=mock.Mock(),
19 | to=mock.Mock(),
20 | state=mock.Mock(),
21 | figi=mock.Mock(),
22 | )
23 | operations_service.get_operations.assert_called_once()
24 |
25 |
26 | def test_get_portfolio(operations_service):
27 | response = operations_service.get_portfolio( # noqa: F841
28 | account_id=mock.Mock(),
29 | )
30 | operations_service.get_portfolio.assert_called_once()
31 |
32 |
33 | def test_get_positions(operations_service):
34 | response = operations_service.get_positions( # noqa: F841
35 | account_id=mock.Mock(),
36 | )
37 | operations_service.get_positions.assert_called_once()
38 |
39 |
40 | def test_get_withdraw_limits(operations_service):
41 | response = operations_service.get_withdraw_limits( # noqa: F841
42 | account_id=mock.Mock(),
43 | )
44 | operations_service.get_withdraw_limits.assert_called_once()
45 |
--------------------------------------------------------------------------------
/examples/instrument_cache.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from pprint import pprint
4 |
5 | from tinkoff.invest import Client, InstrumentIdType
6 | from tinkoff.invest.caching.instruments_cache.instruments_cache import InstrumentsCache
7 | from tinkoff.invest.caching.instruments_cache.settings import InstrumentsCacheSettings
8 |
9 | TOKEN = os.environ["INVEST_TOKEN"]
10 |
11 |
12 | logging.basicConfig(level=logging.INFO)
13 |
14 |
15 | def main():
16 | with Client(TOKEN) as client:
17 | inst = client.instruments.etfs().instruments[-1]
18 | pprint(inst)
19 |
20 | from_server = client.instruments.etf_by(
21 | id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_UID,
22 | class_code=inst.class_code,
23 | id=inst.uid,
24 | )
25 | pprint(from_server)
26 |
27 | settings = InstrumentsCacheSettings()
28 | instruments_cache = InstrumentsCache(
29 | settings=settings, instruments_service=client.instruments
30 | )
31 |
32 | from_cache = instruments_cache.etf_by(
33 | id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_UID,
34 | class_code=inst.class_code,
35 | id=inst.uid,
36 | )
37 | pprint(from_cache)
38 |
39 | if str(from_server) != str(from_cache):
40 | raise Exception("cache miss")
41 |
42 |
43 | if __name__ == "__main__":
44 | main()
45 |
--------------------------------------------------------------------------------
/tests/test_stoporders.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=redefined-outer-name,unused-variable
2 |
3 | from unittest import mock
4 |
5 | import pytest
6 |
7 | from tinkoff.invest.services import StopOrdersService
8 |
9 |
10 | @pytest.fixture()
11 | def stop_orders_service():
12 | return mock.create_autospec(spec=StopOrdersService)
13 |
14 |
15 | def test_post_stop_order(stop_orders_service):
16 | response = stop_orders_service.post_stop_order( # noqa: F841
17 | figi=mock.Mock(),
18 | quantity=mock.Mock(),
19 | price=mock.Mock(),
20 | stop_price=mock.Mock(),
21 | direction=mock.Mock(),
22 | account_id=mock.Mock(),
23 | expiration_type=mock.Mock(),
24 | stop_order_type=mock.Mock(),
25 | expire_date=mock.Mock(),
26 | order_id=mock.Mock(),
27 | )
28 | stop_orders_service.post_stop_order.assert_called_once()
29 |
30 |
31 | def test_get_stop_orders(stop_orders_service):
32 | response = stop_orders_service.get_stop_orders( # noqa: F841
33 | account_id=mock.Mock(),
34 | )
35 | stop_orders_service.get_stop_orders.assert_called_once()
36 |
37 |
38 | def test_cancel_stop_order(stop_orders_service):
39 | response = stop_orders_service.cancel_stop_order( # noqa: F841
40 | account_id=mock.Mock(),
41 | stop_order_id=mock.Mock(),
42 | )
43 | stop_orders_service.cancel_stop_order.assert_called_once()
44 |
--------------------------------------------------------------------------------
/tests/test_users.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=redefined-outer-name,unused-variable
2 |
3 | from unittest import mock
4 |
5 | import pytest
6 |
7 | from tinkoff.invest.services import UsersService
8 |
9 |
10 | @pytest.fixture()
11 | def users_service():
12 | return mock.create_autospec(spec=UsersService)
13 |
14 |
15 | def test_get_accounts(users_service):
16 | response = users_service.get_accounts() # noqa: F841
17 | users_service.get_accounts.assert_called_once()
18 |
19 |
20 | def test_get_margin_attributes(users_service):
21 | response = users_service.get_margin_attributes( # noqa: F841
22 | account_id=mock.Mock(),
23 | )
24 | users_service.get_margin_attributes.assert_called_once()
25 |
26 |
27 | def test_get_user_tariff(users_service):
28 | response = users_service.get_user_tariff() # noqa: F841
29 | users_service.get_user_tariff.assert_called_once()
30 |
31 |
32 | def test_get_info(users_service):
33 | response = users_service.get_info() # noqa: F841
34 | users_service.get_info.assert_called_once()
35 |
36 |
37 | def test_get_bank_accounts(users_service):
38 | users_service.get_bank_accounts()
39 | users_service.get_bank_accounts.assert_called_once()
40 |
41 |
42 | def test_currency_transfer(users_service):
43 | response = users_service.currency_transfer(request=mock.Mock()) # noqa: F841
44 | users_service.currency_transfer.assert_called_once()
45 |
--------------------------------------------------------------------------------
/examples/order_price.py:
--------------------------------------------------------------------------------
1 | """Example - How to get order price."""
2 |
3 | import logging
4 | import os
5 | from decimal import Decimal
6 |
7 | from tinkoff.invest import Client, GetOrderPriceRequest, OrderDirection
8 | from tinkoff.invest.utils import decimal_to_quotation
9 |
10 | TOKEN = os.environ["INVEST_TOKEN"]
11 |
12 | logger = logging.getLogger(__name__)
13 | logging.basicConfig(level=logging.INFO)
14 |
15 |
16 | INSTRUMENT_ID = "TCS00A105GE2"
17 | QUANTITY = 1
18 | PRICE = 230.1
19 |
20 |
21 | def main():
22 | logger.info("Getting Max Lots")
23 | with Client(TOKEN) as client:
24 | response = client.users.get_accounts()
25 | account, *_ = response.accounts
26 | account_id = account.id
27 |
28 | logger.info(
29 | "Get pre-trade order commission and price for instrument=%s, quantity=%s and price=%s",
30 | INSTRUMENT_ID,
31 | QUANTITY,
32 | PRICE,
33 | )
34 | get_order_price = client.orders.get_order_price(
35 | GetOrderPriceRequest(
36 | account_id=account_id,
37 | instrument_id=INSTRUMENT_ID,
38 | quantity=QUANTITY,
39 | direction=OrderDirection.ORDER_DIRECTION_BUY,
40 | price=decimal_to_quotation(Decimal(PRICE)),
41 | )
42 | )
43 |
44 | print(get_order_price)
45 |
46 |
47 | if __name__ == "__main__":
48 | main()
49 |
--------------------------------------------------------------------------------
/tests/test_orders.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=redefined-outer-name,unused-variable
2 |
3 | from unittest import mock
4 |
5 | import pytest
6 |
7 | from tinkoff.invest.services import OrdersService
8 |
9 |
10 | @pytest.fixture()
11 | def orders_service():
12 | return mock.create_autospec(spec=OrdersService)
13 |
14 |
15 | def test_post_order(orders_service):
16 | response = orders_service.post_order( # noqa: F841
17 | figi=mock.Mock(),
18 | quantity=mock.Mock(),
19 | price=mock.Mock(),
20 | direction=mock.Mock(),
21 | account_id=mock.Mock(),
22 | order_type=mock.Mock(),
23 | order_id=mock.Mock(),
24 | )
25 | orders_service.post_order.assert_called_once()
26 |
27 |
28 | def test_cancel_order(orders_service):
29 | response = orders_service.cancel_order( # noqa: F841
30 | account_id=mock.Mock(),
31 | order_id=mock.Mock(),
32 | )
33 | orders_service.cancel_order.assert_called_once()
34 |
35 |
36 | def test_get_order_state(orders_service):
37 | response = orders_service.get_order_state( # noqa: F841
38 | account_id=mock.Mock(),
39 | order_id=mock.Mock(),
40 | )
41 | orders_service.get_order_state.assert_called_once()
42 |
43 |
44 | def test_get_orders(orders_service):
45 | response = orders_service.get_orders( # noqa: F841
46 | account_id=mock.Mock(),
47 | )
48 | orders_service.get_orders.assert_called_once()
49 |
--------------------------------------------------------------------------------
/examples/async_get_tech_analysis.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | from datetime import timedelta
4 | from decimal import Decimal
5 |
6 | from tinkoff.invest import AsyncClient
7 | from tinkoff.invest.schemas import (
8 | Deviation,
9 | GetTechAnalysisRequest,
10 | IndicatorInterval,
11 | IndicatorType,
12 | Smoothing,
13 | TypeOfPrice,
14 | )
15 | from tinkoff.invest.utils import decimal_to_quotation, now
16 |
17 | TOKEN = os.environ["INVEST_TOKEN"]
18 |
19 |
20 | async def main():
21 | async with AsyncClient(TOKEN) as client:
22 | request = GetTechAnalysisRequest(
23 | indicator_type=IndicatorType.INDICATOR_TYPE_RSI,
24 | instrument_uid="6542a064-6633-44ba-902f-710c97507522",
25 | from_=now() - timedelta(days=7),
26 | to=now(),
27 | interval=IndicatorInterval.INDICATOR_INTERVAL_4_HOUR,
28 | type_of_price=TypeOfPrice.TYPE_OF_PRICE_AVG,
29 | length=42,
30 | deviation=Deviation(
31 | deviation_multiplier=decimal_to_quotation(Decimal(1.0)),
32 | ),
33 | smoothing=Smoothing(fast_length=13, slow_length=7, signal_smoothing=3),
34 | )
35 | response = await client.market_data.get_tech_analysis(request=request)
36 | for indicator in response.technical_indicators:
37 | print(indicator.signal)
38 |
39 |
40 | if __name__ == "__main__":
41 | asyncio.run(main())
42 |
--------------------------------------------------------------------------------
/examples/wiseplat_cancel_all_stop_orders.py:
--------------------------------------------------------------------------------
1 | """Example - How to cancel all stop orders."""
2 | import logging
3 | import os
4 |
5 | from tinkoff.invest import Client
6 | from tinkoff.invest.exceptions import InvestError
7 |
8 | TOKEN = os.environ["INVEST_TOKEN"]
9 |
10 | logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG)
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def main():
15 | """Example - How to cancel all stop orders."""
16 | with Client(TOKEN) as client:
17 | response = client.users.get_accounts()
18 | account, *_ = response.accounts
19 | account_id = account.id
20 |
21 | try:
22 | stop_orders_response = client.stop_orders.get_stop_orders(
23 | account_id=account_id
24 | )
25 | logger.info("Stop Orders: %s", stop_orders_response)
26 | for stop_order in stop_orders_response.stop_orders:
27 | client.stop_orders.cancel_stop_order(
28 | account_id=account_id, stop_order_id=stop_order.stop_order_id
29 | )
30 | logger.info("Stop Order: %s was canceled.", stop_order.stop_order_id)
31 | logger.info(
32 | "Orders: %s", client.stop_orders.get_stop_orders(account_id=account_id)
33 | )
34 | except InvestError as error:
35 | logger.error("Failed to cancel all orders. Error: %s", error)
36 |
37 |
38 | if __name__ == "__main__":
39 | main()
40 |
--------------------------------------------------------------------------------
/examples/easy_async_stream_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import (
5 | AsyncClient,
6 | CandleInstrument,
7 | InfoInstrument,
8 | MarketDataResponse,
9 | SubscriptionInterval,
10 | TradeInstrument,
11 | )
12 | from tinkoff.invest.async_services import AsyncMarketDataStreamManager
13 |
14 | TOKEN = os.environ["INVEST_TOKEN"]
15 |
16 |
17 | async def main():
18 | async with AsyncClient(TOKEN) as client:
19 | market_data_stream: AsyncMarketDataStreamManager = (
20 | client.create_market_data_stream()
21 | )
22 | market_data_stream.candles.waiting_close().subscribe(
23 | [
24 | CandleInstrument(
25 | figi="BBG004730N88",
26 | interval=SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE,
27 | )
28 | ]
29 | )
30 | market_data_stream.trades.subscribe(
31 | [
32 | TradeInstrument(
33 | figi="BBG004730N88",
34 | )
35 | ]
36 | )
37 | async for marketdata in market_data_stream:
38 | marketdata: MarketDataResponse = marketdata
39 | print(marketdata)
40 | market_data_stream.info.subscribe([InfoInstrument(figi="BBG004730N88")])
41 | if marketdata.subscribe_info_response:
42 | market_data_stream.stop()
43 |
44 |
45 | if __name__ == "__main__":
46 | asyncio.run(main())
47 |
--------------------------------------------------------------------------------
/tinkoff/invest/market_data_stream/market_data_stream_interface.py:
--------------------------------------------------------------------------------
1 | import abc
2 | from typing import Generic
3 |
4 | from tinkoff.invest.market_data_stream.stream_managers import (
5 | CandlesStreamManager,
6 | InfoStreamManager,
7 | LastPriceStreamManager,
8 | OrderBookStreamManager,
9 | TradesStreamManager,
10 | )
11 | from tinkoff.invest.market_data_stream.typevars import TMarketDataStreamManager
12 | from tinkoff.invest.schemas import MarketDataRequest
13 |
14 |
15 | class IMarketDataStreamManager(abc.ABC, Generic[TMarketDataStreamManager]):
16 | @property
17 | @abc.abstractmethod
18 | def candles(self) -> CandlesStreamManager[TMarketDataStreamManager]:
19 | pass
20 |
21 | @property
22 | @abc.abstractmethod
23 | def order_book(self) -> OrderBookStreamManager[TMarketDataStreamManager]:
24 | pass
25 |
26 | @property
27 | @abc.abstractmethod
28 | def trades(self) -> TradesStreamManager[TMarketDataStreamManager]:
29 | pass
30 |
31 | @property
32 | @abc.abstractmethod
33 | def info(self) -> InfoStreamManager[TMarketDataStreamManager]:
34 | pass
35 |
36 | @property
37 | @abc.abstractmethod
38 | def last_price(self) -> LastPriceStreamManager[TMarketDataStreamManager]:
39 | pass
40 |
41 | @abc.abstractmethod
42 | def subscribe(self, market_data_request: MarketDataRequest) -> None:
43 | pass
44 |
45 | @abc.abstractmethod
46 | def unsubscribe(self, market_data_request: MarketDataRequest) -> None:
47 | pass
48 |
49 | @abc.abstractmethod
50 | def stop(self) -> None:
51 | pass
52 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/account_manager.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from decimal import Decimal
3 |
4 | from tinkoff.invest import Quotation
5 | from tinkoff.invest.services import Services
6 | from tinkoff.invest.strategies.base.errors import (
7 | InsufficientMarginalTradeFunds,
8 | MarginalTradeIsNotActive,
9 | )
10 | from tinkoff.invest.strategies.base.strategy_settings_base import StrategySettings
11 | from tinkoff.invest.utils import quotation_to_decimal
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | class AccountManager:
17 | def __init__(self, services: Services, strategy_settings: StrategySettings):
18 | self._services = services
19 | self._strategy_settings = strategy_settings
20 |
21 | def get_current_balance(self) -> Decimal:
22 | account_id = self._strategy_settings.account_id
23 | portfolio_response = self._services.operations.get_portfolio(
24 | account_id=account_id
25 | )
26 | balance = portfolio_response.total_amount_currencies
27 | return quotation_to_decimal(Quotation(units=balance.units, nano=balance.nano))
28 |
29 | def ensure_marginal_trade(self) -> None:
30 | account_id = self._strategy_settings.account_id
31 | try:
32 | response = self._services.users.get_margin_attributes(account_id=account_id)
33 | except Exception as e:
34 | raise MarginalTradeIsNotActive() from e
35 | value = quotation_to_decimal(response.funds_sufficiency_level)
36 | if value <= 1:
37 | raise InsufficientMarginalTradeFunds()
38 | logger.info("Marginal trade is active")
39 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/market_data_cache/cache_settings.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import dataclasses
3 | import enum
4 | import logging
5 | import os
6 | import pickle # noqa:S403 # nosec
7 | from pathlib import Path
8 | from typing import Dict, Generator, Sequence
9 |
10 | from tinkoff.invest.caching.market_data_cache.datetime_range import DatetimeRange
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | class MarketDataCacheFormat(str, enum.Enum):
16 | CSV = "csv"
17 |
18 |
19 | @dataclasses.dataclass()
20 | class MarketDataCacheSettings:
21 | base_cache_dir: Path = Path(os.getcwd()) / ".market_data_cache"
22 | format_extension: MarketDataCacheFormat = MarketDataCacheFormat.CSV
23 | field_names: Sequence[str] = (
24 | "time",
25 | "open",
26 | "high",
27 | "low",
28 | "close",
29 | "volume",
30 | "is_complete",
31 | "candle_source",
32 | )
33 | meta_extension: str = "meta"
34 |
35 |
36 | @dataclasses.dataclass()
37 | class FileMetaData:
38 | cached_range_in_file: Dict[DatetimeRange, Path]
39 |
40 |
41 | @contextlib.contextmanager
42 | def meta_file_context(meta_file_path: Path) -> Generator[FileMetaData, None, None]:
43 | try:
44 | with open(meta_file_path, "rb") as f:
45 | meta = pickle.load(f) # noqa:S301 # nosec
46 | except FileNotFoundError:
47 | logger.error("File %s was not found. Creating default.", meta_file_path)
48 |
49 | meta = FileMetaData(cached_range_in_file={})
50 | try:
51 | yield meta
52 | finally:
53 | with open(meta_file_path, "wb") as f:
54 | pickle.dump(meta, f)
55 |
--------------------------------------------------------------------------------
/tinkoff/invest/channels.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | from typing import Any, Optional, Sequence
3 |
4 | import grpc
5 | from grpc.aio import ClientInterceptor
6 |
7 | from .constants import INVEST_GRPC_API, MAX_RECEIVE_MESSAGE_LENGTH
8 | from .typedefs import ChannelArgumentType
9 |
10 | __all__ = ("create_channel",)
11 |
12 |
13 | MAX_RECEIVE_MESSAGE_LENGTH_OPTION = "grpc.max_receive_message_length"
14 |
15 |
16 | def create_channel(
17 | *,
18 | target: Optional[str] = None,
19 | options: Optional[ChannelArgumentType] = None,
20 | force_async: bool = False,
21 | compression: Optional[grpc.Compression] = None,
22 | interceptors: Optional[Sequence[ClientInterceptor]] = None,
23 | ) -> Any:
24 | creds = grpc.ssl_channel_credentials()
25 | target = target or INVEST_GRPC_API
26 | if options is None:
27 | options = []
28 |
29 | options = _with_max_receive_message_length_option(options)
30 |
31 | args = (target, creds, options, compression)
32 | if force_async:
33 | return grpc.aio.secure_channel(*args, interceptors)
34 | return grpc.secure_channel(*args)
35 |
36 |
37 | def _with_max_receive_message_length_option(
38 | options: ChannelArgumentType,
39 | ) -> ChannelArgumentType:
40 | if not _contains_option(options, MAX_RECEIVE_MESSAGE_LENGTH_OPTION):
41 | option = (MAX_RECEIVE_MESSAGE_LENGTH_OPTION, MAX_RECEIVE_MESSAGE_LENGTH)
42 | return list(itertools.chain(options, [option]))
43 | return options
44 |
45 |
46 | def _contains_option(options: ChannelArgumentType, expected_option_name: str) -> bool:
47 | for option_name, _ in options:
48 | if option_name == expected_option_name:
49 | return True
50 | return False
51 |
--------------------------------------------------------------------------------
/scripts/version.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import Tuple
3 |
4 | from tomlkit import loads
5 |
6 | Version = Tuple[str, str, str, str, str]
7 |
8 |
9 | def main() -> None:
10 | current_version = get_current_version()
11 | print( # noqa:T201,T001
12 | version_to_str(next_beta_version(parse_version(version=current_version)))
13 | )
14 |
15 |
16 | def get_current_version():
17 | with open("pyproject.toml", "r", encoding="utf-8") as f:
18 | pyproject = loads(f.read())
19 | current_version: str = pyproject["tool"]["poetry"]["version"] # type:ignore
20 | return current_version
21 |
22 |
23 | def parse_version(version: str) -> Version:
24 | pattern = re.compile(
25 | r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" # noqa:E501 # pylint:disable=line-too-long
26 | )
27 | match = pattern.search(version)
28 | if not match:
29 | raise ValueError(f"{version} is not a version")
30 |
31 | return tuple(n and str(n) or "" for n in match.groups(0)) # type:ignore
32 |
33 |
34 | def next_beta_version(version: Version) -> Version:
35 | major, minor, patch, prerelease, buildmetadata = version
36 | if not prerelease:
37 | return major, minor, patch, prerelease, buildmetadata
38 | prerelease_n = int(remove_prefix(prerelease, "beta"))
39 | return (major, minor, patch, "beta" + str(prerelease_n + 1), buildmetadata)
40 |
41 |
42 | def version_to_str(version: Version) -> str:
43 | major, minor, patch, prerelease, _ = version
44 | return f"{major}.{minor}.{patch}-{prerelease}"
45 |
46 |
47 | def remove_prefix(text: str, prefix: str) -> str:
48 | if text.startswith(prefix):
49 | prefix_len = len(prefix)
50 | return text[prefix_len:]
51 | return text
52 |
53 |
54 | if __name__ == "__main__":
55 | main()
56 |
--------------------------------------------------------------------------------
/examples/instruments/instrument_favorites.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from tinkoff.invest import Client
4 | from tinkoff.invest.schemas import (
5 | CreateFavoriteGroupRequest,
6 | DeleteFavoriteGroupRequest,
7 | EditFavoritesActionType as At,
8 | EditFavoritesRequestInstrument,
9 | GetFavoriteGroupsRequest,
10 | )
11 |
12 | TOKEN = os.environ["INVEST_TOKEN"]
13 |
14 |
15 | def main():
16 | with Client(TOKEN) as client:
17 | r = client.instruments.get_favorites()
18 |
19 | print("Список избранных инструментов:")
20 | for i in r.favorite_instruments:
21 | print(f"{i.uid} - {i.name}")
22 |
23 | request = CreateFavoriteGroupRequest()
24 | request.group_name = "My test favorite group"
25 | request.group_color = "aa0000" # red color
26 | r = client.instruments.create_favorite_group(request=request)
27 | group_id = r.group_id
28 | print(f"Создана новая группа избранного с ИД: {group_id}")
29 |
30 | client.instruments.edit_favorites(
31 | instruments=[EditFavoritesRequestInstrument(instrument_id="BBG001M2SC01")],
32 | action_type=At.EDIT_FAVORITES_ACTION_TYPE_ADD,
33 | group_id=group_id,
34 | )
35 |
36 | request = GetFavoriteGroupsRequest()
37 | request.instrument_id = ["BBG001M2SC01"]
38 | r = client.instruments.get_favorite_groups(request=request)
39 | print(f"Список групп избранного:")
40 | for i in r.groups:
41 | print(
42 | f"{i.group_id} - {i.group_name}. Количество элементов: {i.size}. "
43 | f"Содержит выбранный инструмент {request.instrument_id[0]}: "
44 | f"{i.contains_instrument} "
45 | )
46 |
47 | request = DeleteFavoriteGroupRequest()
48 | request.group_id = group_id
49 | client.instruments.delete_favorite_group(request=request)
50 | print(f"Удалена группа избранного с ИД: {group_id}")
51 |
52 |
53 | if __name__ == "__main__":
54 | main()
55 |
--------------------------------------------------------------------------------
/tinkoff/invest/grpc/google/api/field_behavior_pb2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by the protocol buffer compiler. DO NOT EDIT!
3 | # source: tinkoff/invest/grpc/google/api/field_behavior.proto
4 | # Protobuf Python Version: 4.25.1
5 | """Generated protocol buffer code."""
6 | from google.protobuf import (
7 | descriptor as _descriptor,
8 | descriptor_pool as _descriptor_pool,
9 | symbol_database as _symbol_database,
10 | )
11 | from google.protobuf.internal import builder as _builder
12 |
13 | # @@protoc_insertion_point(imports)
14 |
15 | _sym_db = _symbol_database.Default()
16 |
17 |
18 | from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
19 |
20 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n3tinkoff/invest/grpc/google/api/field_behavior.proto\x12\ngoogle.api\x1a google/protobuf/descriptor.proto*\xa6\x01\n\rFieldBehavior\x12\x1e\n\x1a\x46IELD_BEHAVIOR_UNSPECIFIED\x10\x00\x12\x0c\n\x08OPTIONAL\x10\x01\x12\x0c\n\x08REQUIRED\x10\x02\x12\x0f\n\x0bOUTPUT_ONLY\x10\x03\x12\x0e\n\nINPUT_ONLY\x10\x04\x12\r\n\tIMMUTABLE\x10\x05\x12\x12\n\x0eUNORDERED_LIST\x10\x06\x12\x15\n\x11NON_EMPTY_DEFAULT\x10\x07:Q\n\x0e\x66ield_behavior\x12\x1d.google.protobuf.FieldOptions\x18\x9c\x08 \x03(\x0e\x32\x19.google.api.FieldBehaviorBp\n\x0e\x63om.google.apiB\x12\x46ieldBehaviorProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xa2\x02\x04GAPIb\x06proto3')
21 |
22 | _globals = globals()
23 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
24 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tinkoff.invest.grpc.google.api.field_behavior_pb2', _globals)
25 | if _descriptor._USE_C_DESCRIPTORS == False:
26 | _globals['DESCRIPTOR']._options = None
27 | _globals['DESCRIPTOR']._serialized_options = b'\n\016com.google.apiB\022FieldBehaviorProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\242\002\004GAPI'
28 | _globals['_FIELDBEHAVIOR']._serialized_start=102
29 | _globals['_FIELDBEHAVIOR']._serialized_end=268
30 | # @@protoc_insertion_point(module_scope)
31 |
--------------------------------------------------------------------------------
/examples/sandbox/sandbox_post_stop_order.py:
--------------------------------------------------------------------------------
1 | import os
2 | import uuid
3 |
4 | from _decimal import Decimal
5 |
6 | from tinkoff.invest import (
7 | PostStopOrderRequest,
8 | PostStopOrderRequestTrailingData,
9 | PostStopOrderResponse,
10 | StopOrderExpirationType,
11 | StopOrderType,
12 | )
13 | from tinkoff.invest.sandbox.client import SandboxClient
14 | from tinkoff.invest.schemas import Quotation, StopOrderDirection, TrailingValueType
15 | from tinkoff.invest.utils import decimal_to_quotation
16 |
17 |
18 | def main():
19 | token = os.environ["INVEST_TOKEN"]
20 |
21 | with SandboxClient(token) as client:
22 | accounts = client.users.get_accounts()
23 | account_id = accounts.accounts[0].id
24 | response = post_stop_order(
25 | client,
26 | account_id,
27 | "BBG004730ZJ9",
28 | stop_order_direction=StopOrderDirection.STOP_ORDER_DIRECTION_BUY,
29 | quantity=1,
30 | price=Quotation(units=10, nano=0),
31 | )
32 | print(response)
33 |
34 |
35 | def post_stop_order(
36 | sandbox_service, account_id, instrument_id, stop_order_direction, quantity, price
37 | ) -> PostStopOrderResponse:
38 | return sandbox_service.sandbox.post_sandbox_stop_order(
39 | request=PostStopOrderRequest(
40 | account_id=account_id,
41 | instrument_id=instrument_id,
42 | direction=stop_order_direction,
43 | quantity=quantity,
44 | price=price,
45 | order_id=str(uuid.uuid4()),
46 | expiration_type=StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL,
47 | stop_price=price,
48 | stop_order_type=StopOrderType.STOP_ORDER_TYPE_TAKE_PROFIT,
49 | trailing_data=PostStopOrderRequestTrailingData(
50 | indent_type=TrailingValueType.TRAILING_VALUE_RELATIVE,
51 | indent=decimal_to_quotation(Decimal(1)),
52 | ),
53 | )
54 | )
55 |
56 |
57 | if __name__ == "__main__":
58 | main()
59 |
--------------------------------------------------------------------------------
/examples/async_instrument_favorites.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from tinkoff.invest import AsyncClient
5 | from tinkoff.invest.schemas import (
6 | CreateFavoriteGroupRequest,
7 | DeleteFavoriteGroupRequest,
8 | EditFavoritesActionType as At,
9 | EditFavoritesRequestInstrument,
10 | GetFavoriteGroupsRequest,
11 | )
12 |
13 | TOKEN = os.environ["INVEST_TOKEN"]
14 |
15 |
16 | async def main():
17 | async with AsyncClient(TOKEN) as client:
18 | r = await client.instruments.get_favorites()
19 |
20 | print("Список избранных инструментов:")
21 | for i in r.favorite_instruments:
22 | print(f"{i.ticker} - {i.name}")
23 |
24 | request = CreateFavoriteGroupRequest()
25 | request.group_name = "My test favorite group"
26 | request.group_color = "aa0000" # red color
27 | r = await client.instruments.create_favorite_group(request=request)
28 | group_id = r.group_id
29 | print(f"Создана новая группа избранного с ИД: {group_id}")
30 |
31 | await client.instruments.edit_favorites(
32 | instruments=[EditFavoritesRequestInstrument(instrument_id="BBG001M2SC01")],
33 | action_type=At.EDIT_FAVORITES_ACTION_TYPE_ADD,
34 | group_id=group_id,
35 | )
36 |
37 | request = GetFavoriteGroupsRequest()
38 | request.instrument_id = ["BBG001M2SC01"]
39 | r = await client.instruments.get_favorite_groups(request=request)
40 | print(f"Список групп избранного:")
41 | for i in r.groups:
42 | print(
43 | f"{i.group_id} - {i.group_name}. Количество элементов: {i.size}. "
44 | f"Содержит выбранный инструмент {request.instrument_id[0]}: "
45 | f"{i.contains_instrument} "
46 | )
47 |
48 | request = DeleteFavoriteGroupRequest()
49 | request.group_id = group_id
50 | await client.instruments.delete_favorite_group(request=request)
51 | print(f"Удалена группа избранного с ИД: {group_id}")
52 |
53 |
54 | if __name__ == "__main__":
55 | asyncio.run(main())
56 |
--------------------------------------------------------------------------------
/tests/test_protobuf_to_dataclass.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | import pytest
5 |
6 | from tinkoff.invest import (
7 | EditFavoritesActionType,
8 | EditFavoritesRequest as DataclassModel,
9 | )
10 | from tinkoff.invest._grpc_helpers import protobuf_to_dataclass
11 | from tinkoff.invest.grpc.instruments_pb2 import EditFavoritesRequest as ProtoModel
12 |
13 | logging.basicConfig(level=logging.DEBUG)
14 |
15 |
16 | @pytest.fixture()
17 | def unsupported_model() -> ProtoModel:
18 | pb_obj = ProtoModel()
19 | pb_obj.action_type = 137
20 | return pb_obj
21 |
22 |
23 | class TestProtobufToDataclass:
24 | def test_protobuf_to_dataclass_does_not_raise_by_default(
25 | self, unsupported_model: ProtoModel, caplog
26 | ):
27 | expected = EditFavoritesActionType.EDIT_FAVORITES_ACTION_TYPE_UNSPECIFIED
28 |
29 | actual = protobuf_to_dataclass(
30 | pb_obj=unsupported_model, dataclass_type=DataclassModel
31 | ).action_type
32 |
33 | assert expected == actual
34 |
35 | @pytest.mark.parametrize("use_default_enum_if_error", ["True", "true", "1"])
36 | def test_protobuf_to_dataclass_does_not_raise_when_set_true(
37 | self, unsupported_model: ProtoModel, use_default_enum_if_error: str
38 | ):
39 | expected = EditFavoritesActionType.EDIT_FAVORITES_ACTION_TYPE_UNSPECIFIED
40 |
41 | os.environ["USE_DEFAULT_ENUM_IF_ERROR"] = use_default_enum_if_error
42 | actual = protobuf_to_dataclass(
43 | pb_obj=unsupported_model, dataclass_type=DataclassModel
44 | ).action_type
45 |
46 | assert expected == actual
47 |
48 | @pytest.mark.parametrize("use_default_enum_if_error", ["False", "false", "0"])
49 | def test_protobuf_to_dataclass_does_raise_when_set_false(
50 | self, unsupported_model: ProtoModel, use_default_enum_if_error: str
51 | ):
52 | os.environ["USE_DEFAULT_ENUM_IF_ERROR"] = use_default_enum_if_error
53 | with pytest.raises(ValueError):
54 | _ = protobuf_to_dataclass(
55 | pb_obj=unsupported_model, dataclass_type=DataclassModel
56 | ).action_type
57 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/plotting/plotter.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import logging
3 | from typing import Any, Iterable, List, NewType, Protocol
4 |
5 | import matplotlib.pyplot as plt
6 | import mplfinance as mpf
7 | from IPython.display import clear_output
8 | from matplotlib.gridspec import GridSpec
9 |
10 | from tinkoff.invest.strategies.base.event import StrategyEvent
11 |
12 | PlotKwargs = NewType("PlotKwargs", dict)
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | class IPlotter(Protocol):
18 | def plot(self, strategy_events: Iterable[StrategyEvent]) -> None:
19 | pass
20 |
21 |
22 | class StrategyPlotter(abc.ABC, IPlotter):
23 | @abc.abstractmethod
24 | def get_candle_plot_kwargs(
25 | self, strategy_events: List[StrategyEvent]
26 | ) -> PlotKwargs:
27 | pass
28 |
29 | @abc.abstractmethod
30 | def get_signal_plot_kwargs(
31 | self, strategy_events: List[StrategyEvent]
32 | ) -> List[PlotKwargs]:
33 | pass
34 |
35 | def get_plot_kwargs(
36 | self, strategy_events: Iterable[StrategyEvent], ax: Any
37 | ) -> PlotKwargs:
38 | strategy_events = list(strategy_events)
39 | candle_plot = self.get_candle_plot_kwargs(strategy_events=strategy_events)
40 | if signal_plots := self.get_signal_plot_kwargs(strategy_events=strategy_events):
41 | add_plots = []
42 | for signal_plot in signal_plots:
43 | signal_plot.update({"ax": ax})
44 | ap = mpf.make_addplot(**signal_plot)
45 | add_plots.append(ap)
46 |
47 | candle_plot.update({"addplot": add_plots})
48 | return candle_plot
49 |
50 | def plot(self, strategy_events: Iterable[StrategyEvent]) -> None:
51 | _fig = plt.figure(figsize=(20, 20))
52 | gs = GridSpec(2, 1, height_ratios=[3, 1])
53 | _ax1 = plt.subplot(gs[0])
54 | _ax2 = plt.subplot(gs[1])
55 |
56 | candle_plot_kwargs = self.get_plot_kwargs(strategy_events, ax=_ax1)
57 | candle_plot_kwargs.update({"ax": _ax1, "volume": _ax2})
58 | mpf.plot(**candle_plot_kwargs, warn_too_much_data=999999999)
59 |
60 | clear_output(wait=True)
61 | _fig.canvas.draw()
62 | _fig.canvas.flush_events()
63 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/signal_executor_base.py:
--------------------------------------------------------------------------------
1 | from tinkoff.invest import OrderDirection, OrderType
2 | from tinkoff.invest.services import Services
3 | from tinkoff.invest.strategies.base.signal import (
4 | CloseLongMarketOrder,
5 | CloseShortMarketOrder,
6 | OpenLongMarketOrder,
7 | OpenShortMarketOrder,
8 | )
9 | from tinkoff.invest.strategies.base.strategy_settings_base import StrategySettings
10 |
11 |
12 | class SignalExecutor:
13 | def __init__(
14 | self,
15 | services: Services,
16 | settings: StrategySettings,
17 | ):
18 | self._services = services
19 | self._settings = settings
20 |
21 | def execute_open_long_market_order(self, signal: OpenLongMarketOrder) -> None:
22 | self._services.orders.post_order(
23 | figi=self._settings.share_id,
24 | quantity=signal.lots,
25 | direction=OrderDirection.ORDER_DIRECTION_BUY,
26 | account_id=self._settings.account_id,
27 | order_type=OrderType.ORDER_TYPE_MARKET,
28 | )
29 |
30 | def execute_close_long_market_order(self, signal: CloseLongMarketOrder) -> None:
31 | self._services.orders.post_order(
32 | figi=self._settings.share_id,
33 | quantity=signal.lots,
34 | direction=OrderDirection.ORDER_DIRECTION_SELL,
35 | account_id=self._settings.account_id,
36 | order_type=OrderType.ORDER_TYPE_MARKET,
37 | )
38 |
39 | def execute_open_short_market_order(self, signal: OpenShortMarketOrder) -> None:
40 | self._services.orders.post_order(
41 | figi=self._settings.share_id,
42 | quantity=signal.lots,
43 | direction=OrderDirection.ORDER_DIRECTION_SELL,
44 | account_id=self._settings.account_id,
45 | order_type=OrderType.ORDER_TYPE_MARKET,
46 | )
47 |
48 | def execute_close_short_market_order(self, signal: CloseShortMarketOrder) -> None:
49 | self._services.orders.post_order(
50 | figi=self._settings.share_id,
51 | quantity=signal.lots,
52 | direction=OrderDirection.ORDER_DIRECTION_BUY,
53 | account_id=self._settings.account_id,
54 | order_type=OrderType.ORDER_TYPE_MARKET,
55 | )
56 |
--------------------------------------------------------------------------------
/tests/marketdata/test_async_marketdata.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from unittest.mock import ANY, call
3 |
4 | import pytest
5 | import pytest_asyncio
6 |
7 | from tinkoff.invest import CandleInterval, GetCandlesResponse
8 | from tinkoff.invest.async_services import AsyncServices, MarketDataService
9 |
10 |
11 | @pytest_asyncio.fixture
12 | async def marketdata_service(mocker) -> MarketDataService:
13 | return mocker.create_autospec(MarketDataService)
14 |
15 |
16 | @pytest_asyncio.fixture
17 | async def async_services(
18 | mocker, marketdata_service: MarketDataService
19 | ) -> AsyncServices:
20 | async_services = mocker.create_autospec(AsyncServices)
21 | async_services.market_data = marketdata_service
22 | return async_services
23 |
24 |
25 | class TestAsyncMarketData:
26 | @pytest.mark.asyncio
27 | @pytest.mark.parametrize(
28 | "candle_interval,from_,to,expected",
29 | [
30 | (
31 | CandleInterval.CANDLE_INTERVAL_DAY,
32 | datetime(2020, 1, 1),
33 | datetime(2020, 1, 2),
34 | 1,
35 | ),
36 | (
37 | CandleInterval.CANDLE_INTERVAL_DAY,
38 | datetime(2020, 1, 1),
39 | datetime(2021, 3, 3),
40 | 2,
41 | ),
42 | ],
43 | )
44 | async def test_get_candles(
45 | self,
46 | async_services: AsyncServices,
47 | marketdata_service: MarketDataService,
48 | candle_interval: CandleInterval,
49 | from_: datetime,
50 | to: datetime,
51 | expected: int,
52 | ):
53 | marketdata_service.get_candles.return_value = GetCandlesResponse(candles=[])
54 | [
55 | candle
56 | async for candle in AsyncServices.get_all_candles(
57 | async_services, interval=candle_interval, from_=from_, to=to
58 | )
59 | ]
60 | marketdata_service.get_candles.assert_has_calls(
61 | [
62 | call(
63 | from_=ANY,
64 | to=ANY,
65 | interval=candle_interval,
66 | candle_source_type=None,
67 | figi="",
68 | instrument_id="",
69 | )
70 | for _ in range(expected)
71 | ]
72 | )
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tinkoff Invest
2 |
3 | [](https://pypi.org/project/tinkoff-investments/)
4 | [](https://www.python.org/downloads/)
5 | 
6 | 
7 |
8 | Данный репозиторий предоставляет клиент для взаимодействия с торговой платформой [Тинькофф Инвестиции](https://www.tinkoff.ru/invest/) на языке Python.
9 |
10 | - [Документация](https://RussianInvestments.github.io/invest-python/)
11 | - [Документация по Invest API](https://RussianInvestments.github.io/investAPI/)
12 |
13 | ## Начало работы
14 |
15 |
16 |
17 | ```
18 | $ pip install tinkoff-investments
19 | ```
20 |
21 | ## Возможности
22 |
23 | - ☑ Синхронный и асинхронный GRPC клиент
24 | - ☑ Возможность отменить все заявки
25 | - ☑ Выгрузка истории котировок "от" и "до"
26 | - ☑ Кеширование данных
27 | - ☑ Торговая стратегия
28 |
29 | ## Как пользоваться
30 |
31 | ### Получить список аккаунтов
32 |
33 | ```python
34 | from tinkoff.invest import Client
35 |
36 | TOKEN = 'token'
37 |
38 | with Client(TOKEN) as client:
39 | print(client.users.get_accounts())
40 | ```
41 |
42 | ### Переопределить target
43 |
44 | В Tinkoff Invest API есть два контура - "боевой", предназначенный для исполнения ордеров на бирже и "песочница", предназначенный для тестирования API и торговых гипотез, заявки с которого не выводятся на биржу, а исполняются в эмуляторе.
45 |
46 | Переключение между контурами реализовано через target, INVEST_GRPC_API - "боевой", INVEST_GRPC_API_SANDBOX - "песочница"
47 |
48 | ```python
49 | from tinkoff.invest import Client
50 | from tinkoff.invest.constants import INVEST_GRPC_API
51 |
52 | TOKEN = 'token'
53 |
54 | with Client(TOKEN, target=INVEST_GRPC_API) as client:
55 | print(client.users.get_accounts())
56 | ```
57 |
58 | > :warning: **Не публикуйте токены в общедоступные репозитории**
59 |
60 |
61 | Остальные примеры доступны в [examples](https://github.com/RussianInvestments/invest-python/tree/main/examples).
62 |
63 | ## Contribution
64 |
65 | Для тех, кто хочет внести свои изменения в проект.
66 |
67 | - [CONTRIBUTING](https://github.com/RussianInvestments/invest-python/blob/main/CONTRIBUTING.md)
68 |
69 | ## License
70 |
71 | Лицензия [The Apache License](https://github.com/RussianInvestments/invest-python/blob/main/LICENSE).
72 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/moving_average/signal_executor.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from functools import singledispatchmethod
3 |
4 | from tinkoff.invest.services import Services
5 | from tinkoff.invest.strategies.base.errors import UnknownSignal
6 | from tinkoff.invest.strategies.base.signal import (
7 | CloseLongMarketOrder,
8 | CloseShortMarketOrder,
9 | OpenLongMarketOrder,
10 | OpenShortMarketOrder,
11 | Signal,
12 | )
13 | from tinkoff.invest.strategies.base.signal_executor_base import SignalExecutor
14 | from tinkoff.invest.strategies.moving_average.strategy_settings import (
15 | MovingAverageStrategySettings,
16 | )
17 | from tinkoff.invest.strategies.moving_average.strategy_state import (
18 | MovingAverageStrategyState,
19 | )
20 |
21 | logger = logging.getLogger(__name__)
22 |
23 |
24 | class MovingAverageSignalExecutor(SignalExecutor):
25 | def __init__(
26 | self,
27 | services: Services,
28 | state: MovingAverageStrategyState,
29 | settings: MovingAverageStrategySettings,
30 | ):
31 | super().__init__(services, settings)
32 | self._services = services
33 | self._state = state
34 |
35 | @singledispatchmethod
36 | def execute(self, signal: Signal) -> None:
37 | raise UnknownSignal()
38 |
39 | @execute.register
40 | def _execute_open_long_market_order(self, signal: OpenLongMarketOrder) -> None:
41 | self.execute_open_long_market_order(signal)
42 | self._state.long_open = True
43 | self._state.position = signal.lots
44 | logger.info("Signal executed %s", signal)
45 |
46 | @execute.register
47 | def _execute_close_long_market_order(self, signal: CloseLongMarketOrder) -> None:
48 | self.execute_close_long_market_order(signal)
49 | self._state.long_open = False
50 | self._state.position = 0
51 | logger.info("Signal executed %s", signal)
52 |
53 | @execute.register
54 | def _execute_open_short_market_order(self, signal: OpenShortMarketOrder) -> None:
55 | self.execute_open_short_market_order(signal)
56 | self._state.short_open = True
57 | self._state.position = signal.lots
58 | logger.info("Signal executed %s", signal)
59 |
60 | @execute.register
61 | def _execute_close_short_market_order(self, signal: CloseShortMarketOrder) -> None:
62 | self.execute_close_short_market_order(signal)
63 | self._state.short_open = False
64 | self._state.position = 0
65 | logger.info("Signal executed %s", signal)
66 |
--------------------------------------------------------------------------------
/docs/robots.md:
--------------------------------------------------------------------------------
1 | ## Примеры готовых роботов
2 |
3 | | Ссылка на репозиторий | Описание |
4 | |------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
5 | | [tromario/tinkoff-invest-volume-analysis-robot](https://github.com/tromario/tinkoff-invest-volume-analysis-robot) | Проектом был реализован один из методов работы с профилем рынка - реакция на максимальный горизонтальный объем внутри дня за выбранный период.Основной объем работы был заложен в математический аппарат. Работа имеет визуализацию алгоритма. |
6 | | [qwertyo1/tinkoff-trading-bot](https://github.com/qwertyo1/tinkoff-trading-bot) | Проектом реализована простая интервальная стратегия. Несложный код поможет начинающим разработчикам быстро разобраться, запустить, проверить и доработать торговую стратегию под свои цели. Простое ведение статистики через sqllite. |
7 | | [karpp/investRobot](https://github.com/karpp/investRobot) | investRobot - это робот для алгоритмической торговли на бирже Тинькофф Инвестиций посредством Tinkoff Invest API. В качестве демонстрации представлена одна торговая стратегия, основанная на индикаторе двух скользящих средних. |
8 | | [EIDiamond/invest-bot](https://github.com/EIDiamond/invest-bot) | Робот интрадей торговли на Московской бирже с возможность информирования о сделках и результатах торговли в Telegram чат.Удобное решение опционального включения\выключения информирования в Телеграм. Без подключения Телеграм чата все события и результаты пишутся в лог файл. |
9 |
10 | ## Готовые стратегии
11 |
12 | Функция создает дополнительный столбец с действиями ("ma200_support_action"), куда записываются сигналы на шорт или лонг по условиям.
13 | Затем данные агрегируются и выводятся в виде списка акций, по которым пришли сигналы, в порядке убывания даты сигнала.
14 | ~~~python
15 | {% include "../examples/strategies/moving_average.py" %}
16 | ~~~
17 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PYTHONPATH = PYTHONPATH=./
2 | POETRY_RUN = poetry run
3 |
4 | PROTO_DIR = protos/tinkoff/invest/grpc
5 | PACKAGE_PROTO_DIR = tinkoff/invest/grpc
6 | OUT = .
7 | PROTOS = protos
8 |
9 | TEST = $(POETRY_RUN) pytest $(args)
10 | MAIN_CODE = tinkoff examples scripts
11 | CODE = tests $(MAIN_CODE)
12 |
13 | .PHONY: test
14 | test:
15 | $(TEST) --cov
16 |
17 | .PHONY: test-fast
18 | test-fast:
19 | $(TEST)
20 |
21 | .PHONY: test-sandbox
22 | test-sandbox:
23 | $(TEST) --test-sandbox --cov
24 |
25 | .PHONY: lint
26 | lint:
27 | $(POETRY_RUN) ruff $(CODE)
28 | $(POETRY_RUN) black --check $(CODE)
29 | $(POETRY_RUN) pytest --dead-fixtures --dup-fixtures
30 | $(POETRY_RUN) mypy $(MAIN_CODE)
31 | $(POETRY_RUN) poetry check
32 |
33 | .PHONY: format
34 | format:
35 | $(POETRY_RUN) isort $(CODE)
36 | $(POETRY_RUN) black $(CODE)
37 | $(POETRY_RUN) ruff --fix $(CODE)
38 |
39 | .PHONY: check
40 | check: lint test
41 |
42 | .PHONY: docs
43 | docs:
44 | mkdir -p ./docs
45 | cp README.md ./docs/
46 | cp CHANGELOG.md ./docs/
47 | cp CONTRIBUTING.md ./docs/
48 | $(POETRY_RUN) mkdocs build -s -v
49 |
50 | .PHONY: docs-serve
51 | docs-serve:
52 | $(POETRY_RUN) mkdocs serve
53 |
54 | .PHONY: next-version
55 | next-version:
56 | @$(POETRY_RUN) python -m scripts.version
57 |
58 | .PHONY: bump-version
59 | bump-version:
60 | poetry version $(v)
61 | $(POETRY_RUN) python -m scripts.update_package_version $(v)
62 | $(POETRY_RUN) python -m scripts.update_issue_templates $(v)
63 | git add . && git commit -m "chore(release): bump version to $(v)"
64 | git tag -a $(v) -m ""
65 |
66 | .PHONY: install-poetry
67 | install-poetry:
68 | pip install poetry==1.7.1
69 |
70 | .PHONY: install-docs
71 | install-docs:
72 | poetry install --only docs
73 |
74 | .PHONY: install-bump
75 | install-bump:
76 | poetry install --only bump
77 |
78 | .PHONY: install
79 | install:
80 | poetry install -E all
81 |
82 | .PHONY: publish
83 | publish:
84 | @poetry publish --build --no-interaction --username=$(pypi_username) --password=$(pypi_password)
85 |
86 | .PHONY: download-protos
87 | download-protos:
88 | $(POETRY_RUN) python -m scripts.download_protos
89 |
90 | .PHONY: gen-grpc
91 | gen-grpc:
92 | rm -r ${PACKAGE_PROTO_DIR}
93 | $(POETRY_RUN) python -m grpc_tools.protoc -I${PROTOS} --python_out=${OUT} --mypy_out=${OUT} --grpc_python_out=${OUT} ${PROTO_DIR}/google/api/*.proto
94 | $(POETRY_RUN) python -m grpc_tools.protoc -I${PROTOS} --python_out=${OUT} --mypy_out=${OUT} --grpc_python_out=${OUT} ${PROTO_DIR}/*.proto
95 | touch ${PACKAGE_PROTO_DIR}/__init__.py
96 |
97 | .PHONY: gen-client
98 | gen-client: download-protos gen-grpc
99 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/instruments_cache/interface.py:
--------------------------------------------------------------------------------
1 | import abc
2 |
3 | from tinkoff.invest import (
4 | BondResponse,
5 | BondsResponse,
6 | CurrenciesResponse,
7 | CurrencyResponse,
8 | EtfResponse,
9 | EtfsResponse,
10 | FutureResponse,
11 | FuturesResponse,
12 | InstrumentIdType,
13 | InstrumentStatus,
14 | ShareResponse,
15 | SharesResponse,
16 | )
17 |
18 |
19 | class IInstrumentsGetter(abc.ABC):
20 | @abc.abstractmethod
21 | def shares(
22 | self, *, instrument_status: InstrumentStatus = InstrumentStatus(0)
23 | ) -> SharesResponse:
24 | pass
25 |
26 | @abc.abstractmethod
27 | def share_by(
28 | self,
29 | *,
30 | id_type: InstrumentIdType = InstrumentIdType(0),
31 | class_code: str = "",
32 | id: str = "",
33 | ) -> ShareResponse:
34 | pass
35 |
36 | @abc.abstractmethod
37 | def futures(
38 | self, *, instrument_status: InstrumentStatus = InstrumentStatus(0)
39 | ) -> FuturesResponse:
40 | pass
41 |
42 | @abc.abstractmethod
43 | def future_by(
44 | self,
45 | *,
46 | id_type: InstrumentIdType = InstrumentIdType(0),
47 | class_code: str = "",
48 | id: str = "",
49 | ) -> FutureResponse:
50 | pass
51 |
52 | @abc.abstractmethod
53 | def etfs(
54 | self, *, instrument_status: InstrumentStatus = InstrumentStatus(0)
55 | ) -> EtfsResponse:
56 | pass
57 |
58 | @abc.abstractmethod
59 | def etf_by(
60 | self,
61 | *,
62 | id_type: InstrumentIdType = InstrumentIdType(0),
63 | class_code: str = "",
64 | id: str = "",
65 | ) -> EtfResponse:
66 | pass
67 |
68 | @abc.abstractmethod
69 | def bonds(
70 | self, *, instrument_status: InstrumentStatus = InstrumentStatus(0)
71 | ) -> BondsResponse:
72 | pass
73 |
74 | @abc.abstractmethod
75 | def bond_by(
76 | self,
77 | *,
78 | id_type: InstrumentIdType = InstrumentIdType(0),
79 | class_code: str = "",
80 | id: str = "",
81 | ) -> BondResponse:
82 | pass
83 |
84 | @abc.abstractmethod
85 | def currencies(
86 | self, *, instrument_status: InstrumentStatus = InstrumentStatus(0)
87 | ) -> CurrenciesResponse:
88 | pass
89 |
90 | @abc.abstractmethod
91 | def currency_by(
92 | self,
93 | *,
94 | id_type: InstrumentIdType = InstrumentIdType(0),
95 | class_code: str = "",
96 | id: str = "",
97 | ) -> CurrencyResponse:
98 | pass
99 |
--------------------------------------------------------------------------------
/examples/trailing_stop.py:
--------------------------------------------------------------------------------
1 | """Example - Trailing Stop Take Profit order.
2 | spread=0.5 relative value
3 | indent=0.5 absolute value
4 | """
5 |
6 | import json
7 | import logging
8 | import os
9 | from decimal import Decimal
10 |
11 | from tinkoff.invest import (
12 | Client,
13 | ExchangeOrderType,
14 | GetStopOrdersRequest,
15 | PostStopOrderRequest,
16 | PostStopOrderRequestTrailingData,
17 | StopOrderDirection,
18 | StopOrderExpirationType,
19 | StopOrderTrailingData,
20 | StopOrderType,
21 | TakeProfitType,
22 | )
23 | from tinkoff.invest.schemas import TrailingValueType
24 | from tinkoff.invest.utils import decimal_to_quotation
25 |
26 | TOKEN = os.environ["INVEST_TOKEN"]
27 |
28 | logger = logging.getLogger(__name__)
29 | logging.basicConfig(level=logging.INFO)
30 |
31 |
32 | INSTRUMENT_ID = "TCS00A105GE2"
33 | QUANTITY = 1
34 | PRICE = 230.500000000
35 | STOPPRICE = 230
36 | INDENT = 0.5
37 | SPREAD = 0.5
38 |
39 |
40 | def main():
41 | logger.info("Getting Max Lots")
42 | with Client(TOKEN) as client:
43 | response = client.users.get_accounts()
44 | account, *_ = response.accounts
45 | account_id = account.id
46 |
47 | logger.info(
48 | "Post take profit stop order for instrument=%s and trailing parameters: indent=%s, spread=%s, price=%s ",
49 | INSTRUMENT_ID,
50 | INDENT,
51 | SPREAD,
52 | STOPPRICE,
53 | )
54 |
55 | post_stop_order = client.stop_orders.post_stop_order(
56 | quantity=QUANTITY,
57 | price=decimal_to_quotation(Decimal(PRICE)),
58 | stop_price=decimal_to_quotation(Decimal(STOPPRICE)),
59 | direction=StopOrderDirection.STOP_ORDER_DIRECTION_SELL,
60 | account_id=account_id,
61 | stop_order_type=StopOrderType.STOP_ORDER_TYPE_TAKE_PROFIT,
62 | instrument_id=INSTRUMENT_ID,
63 | expiration_type=StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL,
64 | exchange_order_type=ExchangeOrderType.EXCHANGE_ORDER_TYPE_LIMIT,
65 | take_profit_type=TakeProfitType.TAKE_PROFIT_TYPE_TRAILING,
66 | trailing_data=StopOrderTrailingData(
67 | indent=decimal_to_quotation(Decimal(INDENT)),
68 | indent_type=TrailingValueType.TRAILING_VALUE_ABSOLUTE,
69 | spread=decimal_to_quotation(Decimal(SPREAD)),
70 | spread_type=TrailingValueType.TRAILING_VALUE_RELATIVE,
71 | ),
72 | )
73 |
74 | print(post_stop_order)
75 |
76 |
77 | if __name__ == "__main__":
78 | main()
79 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | # pylint:disable=protected-access
2 | from datetime import datetime
3 |
4 | import pytest
5 |
6 | from tinkoff.invest.schemas import CandleInterval
7 | from tinkoff.invest.utils import empty_or_uuid, get_intervals
8 |
9 |
10 | @pytest.mark.parametrize(
11 | ("candle_interval", "interval", "intervals"),
12 | [
13 | (
14 | CandleInterval.CANDLE_INTERVAL_DAY,
15 | (datetime(2021, 1, 25, 0, 0), datetime(2022, 1, 25, 0, 1)),
16 | [
17 | (
18 | datetime(2021, 1, 25, 0, 0),
19 | datetime(2022, 1, 25, 0, 0),
20 | )
21 | ],
22 | ),
23 | (
24 | CandleInterval.CANDLE_INTERVAL_DAY,
25 | (datetime(2021, 1, 25, 0, 0), datetime(2023, 2, 26, 0, 1)),
26 | [
27 | (
28 | datetime(2021, 1, 25, 0, 0),
29 | datetime(2022, 1, 25, 0, 0),
30 | ),
31 | (
32 | datetime(2022, 1, 26, 0, 0),
33 | datetime(2023, 1, 26, 0, 0),
34 | ),
35 | (
36 | datetime(2023, 1, 27, 0, 0),
37 | datetime(2023, 2, 26, 0, 1),
38 | ),
39 | ],
40 | ),
41 | (
42 | CandleInterval.CANDLE_INTERVAL_DAY,
43 | (datetime(2021, 1, 25, 0, 0), datetime(2022, 1, 25, 0, 0)),
44 | [
45 | (
46 | datetime(2021, 1, 25, 0, 0),
47 | datetime(2022, 1, 25, 0, 0),
48 | ),
49 | ],
50 | ),
51 | (
52 | CandleInterval.CANDLE_INTERVAL_DAY,
53 | (datetime(2021, 1, 25, 0, 0), datetime(2022, 1, 24, 0, 0)),
54 | [
55 | (
56 | datetime(2021, 1, 25, 0, 0),
57 | datetime(2022, 1, 24, 0, 0),
58 | ),
59 | ],
60 | ),
61 | ],
62 | )
63 | def test_get_intervals(candle_interval, interval, intervals):
64 | result = list(
65 | get_intervals(
66 | candle_interval,
67 | *interval,
68 | )
69 | )
70 |
71 | assert result == intervals
72 |
73 |
74 | @pytest.mark.parametrize(
75 | "s, expected",
76 | [
77 | ("", True),
78 | ("123", False),
79 | ("1234567890", False),
80 | ("12345678-1234-1234-1234-abcdabcdabcd", True),
81 | ("12345678-12g4-1234-1234-abcdabcdabcd", False),
82 | ],
83 | )
84 | def test_is_empty_or_uuid(s: str, expected: bool):
85 | assert expected == empty_or_uuid(s)
86 |
--------------------------------------------------------------------------------
/tinkoff/invest/strategies/base/trader_base.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import logging
3 | from datetime import timedelta
4 | from typing import Iterable
5 |
6 | import tinkoff
7 | from tinkoff.invest import HistoricCandle
8 | from tinkoff.invest.services import Services
9 | from tinkoff.invest.strategies.base.models import Candle, CandleEvent
10 | from tinkoff.invest.strategies.base.strategy_interface import InvestStrategy
11 | from tinkoff.invest.strategies.base.strategy_settings_base import StrategySettings
12 | from tinkoff.invest.strategies.base.trader_interface import ITrader
13 | from tinkoff.invest.utils import now, quotation_to_decimal
14 |
15 | logger = logging.getLogger(__name__)
16 |
17 |
18 | class Trader(ITrader, abc.ABC):
19 | def __init__(
20 | self,
21 | strategy: InvestStrategy,
22 | services: Services,
23 | settings: StrategySettings,
24 | ):
25 | self._strategy = strategy
26 | self._services = services
27 | self._settings = settings
28 |
29 | @staticmethod
30 | def _convert_historic_candles_into_candle_events(
31 | historic_candles: Iterable[HistoricCandle],
32 | ) -> Iterable[CandleEvent]:
33 | for candle in historic_candles:
34 | yield CandleEvent(
35 | candle=Candle(
36 | open=quotation_to_decimal(candle.open),
37 | close=quotation_to_decimal(candle.close),
38 | high=quotation_to_decimal(candle.high),
39 | low=quotation_to_decimal(candle.low),
40 | ),
41 | volume=candle.volume,
42 | time=candle.time,
43 | is_complete=candle.is_complete,
44 | )
45 |
46 | def _load_candles(self, period: timedelta) -> Iterable[CandleEvent]:
47 | logger.info("Loading candles for period %s from %s", period, now())
48 |
49 | yield from self._convert_historic_candles_into_candle_events(
50 | self._services.get_all_candles(
51 | figi=self._settings.share_id,
52 | from_=now() - period,
53 | interval=self._settings.candle_interval,
54 | )
55 | )
56 |
57 | @staticmethod
58 | def _convert_candle(candle: tinkoff.invest.schemas.Candle) -> CandleEvent:
59 | return CandleEvent(
60 | candle=Candle(
61 | open=quotation_to_decimal(candle.open),
62 | close=quotation_to_decimal(candle.close),
63 | high=quotation_to_decimal(candle.high),
64 | low=quotation_to_decimal(candle.low),
65 | ),
66 | volume=candle.volume,
67 | time=candle.time,
68 | is_complete=False,
69 | )
70 |
--------------------------------------------------------------------------------
/scripts/download_protos.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import sys
4 | from http import HTTPStatus
5 | from pathlib import Path
6 | from zipfile import ZipFile
7 |
8 | import requests
9 |
10 | BRANCH = "main"
11 |
12 | URL = f"https://github.com/RussianInvestments/investAPI/archive/refs/heads/{BRANCH}.zip"
13 | OUTPUT_PATH = "protos/tinkoff/invest/grpc"
14 | PROTOS_TMP_ZIP = "protos.zip"
15 | ZIP_PROTOS_ROOT_PATH_BRANCH = BRANCH.replace("/", "-")
16 | ZIP_PROTOS_ROOT_PATH = f"investAPI-{ZIP_PROTOS_ROOT_PATH_BRANCH}"
17 | ZIP_PROTOS_PATH = f"{ZIP_PROTOS_ROOT_PATH}/src/docs/contracts"
18 | FILES = [
19 | "google/api/field_behavior.proto",
20 | "common.proto",
21 | "instruments.proto",
22 | "marketdata.proto",
23 | "operations.proto",
24 | "orders.proto",
25 | "sandbox.proto",
26 | "signals.proto",
27 | "stoporders.proto",
28 | "users.proto",
29 | ]
30 |
31 | LINES_TO_REPLACE = [
32 | (f'import "{file_name}";', f'import "tinkoff/invest/grpc/{file_name}";')
33 | for file_name in FILES
34 | ]
35 |
36 |
37 | def main() -> int:
38 | _clear_in_start()
39 | _download_protos()
40 | _extract_protos()
41 | _move_protos()
42 | _clear_in_end()
43 | _modify_protos()
44 | return 0
45 |
46 |
47 | def _clear_in_start():
48 | shutil.rmtree(OUTPUT_PATH, ignore_errors=True)
49 |
50 |
51 | def _download_protos():
52 | session = requests.session()
53 | response = session.get(URL, stream=True)
54 | if response.status_code != HTTPStatus.OK:
55 | return
56 |
57 | with open(PROTOS_TMP_ZIP, "wb") as f:
58 | for chunk in response:
59 | f.write(chunk)
60 |
61 |
62 | def _extract_protos():
63 | with ZipFile(PROTOS_TMP_ZIP) as zf:
64 | for name in FILES:
65 | zf.extract(f"{ZIP_PROTOS_PATH}/{name}", path=".")
66 |
67 |
68 | def _move_protos():
69 | os.makedirs(OUTPUT_PATH, exist_ok=True)
70 | for name in FILES:
71 | folders = "/".join(name.split("/")[:-1])
72 | Path(f"{OUTPUT_PATH}/{folders}").mkdir(parents=True, exist_ok=True)
73 | shutil.move(f"{ZIP_PROTOS_PATH}/{name}", f"{OUTPUT_PATH}/{folders}")
74 |
75 |
76 | def _clear_in_end():
77 | os.remove(PROTOS_TMP_ZIP)
78 | shutil.rmtree(ZIP_PROTOS_ROOT_PATH)
79 |
80 |
81 | def _modify_protos():
82 | for name in FILES:
83 | with open(f"{OUTPUT_PATH}/{name}", "r", encoding="utf-8") as f:
84 | protofile_text = f.read()
85 |
86 | for str_to_replace, replaced_str in LINES_TO_REPLACE:
87 | protofile_text = protofile_text.replace(str_to_replace, replaced_str)
88 |
89 | with open(f"{OUTPUT_PATH}/{name}", "w+", encoding="utf-8") as f:
90 | f.write(protofile_text)
91 |
92 |
93 | if __name__ == "__main__":
94 | sys.exit(main())
95 |
--------------------------------------------------------------------------------
/examples/wiseplat_get_figi_for_ticker.py:
--------------------------------------------------------------------------------
1 | """Example - How to get figi by name of ticker."""
2 | import logging
3 | import os
4 |
5 | from pandas import DataFrame
6 |
7 | from tinkoff.invest import Client, SecurityTradingStatus
8 | from tinkoff.invest.services import InstrumentsService
9 | from tinkoff.invest.utils import quotation_to_decimal
10 |
11 | TOKEN = os.environ["INVEST_TOKEN"]
12 |
13 | logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG)
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | def main():
18 | """Example - How to get figi by name of ticker."""
19 |
20 | ticker = "VTBR" # "BRH3" "SBER" "VTBR"
21 |
22 | with Client(TOKEN) as client:
23 | instruments: InstrumentsService = client.instruments
24 | tickers = []
25 | for method in ["shares", "bonds", "etfs", "currencies", "futures"]:
26 | for item in getattr(instruments, method)().instruments:
27 | tickers.append(
28 | {
29 | "name": item.name,
30 | "ticker": item.ticker,
31 | "class_code": item.class_code,
32 | "figi": item.figi,
33 | "uid": item.uid,
34 | "type": method,
35 | "min_price_increment": quotation_to_decimal(
36 | item.min_price_increment
37 | ),
38 | "scale": 9 - len(str(item.min_price_increment.nano)) + 1,
39 | "lot": item.lot,
40 | "trading_status": str(
41 | SecurityTradingStatus(item.trading_status).name
42 | ),
43 | "api_trade_available_flag": item.api_trade_available_flag,
44 | "currency": item.currency,
45 | "exchange": item.exchange,
46 | "buy_available_flag": item.buy_available_flag,
47 | "sell_available_flag": item.sell_available_flag,
48 | "short_enabled_flag": item.short_enabled_flag,
49 | "klong": quotation_to_decimal(item.klong),
50 | "kshort": quotation_to_decimal(item.kshort),
51 | }
52 | )
53 |
54 | tickers_df = DataFrame(tickers)
55 |
56 | ticker_df = tickers_df[tickers_df["ticker"] == ticker]
57 | if ticker_df.empty:
58 | logger.error("There is no such ticker: %s", ticker)
59 | return
60 |
61 | figi = ticker_df["figi"].iloc[0]
62 | print(f"\nTicker {ticker} have figi={figi}\n")
63 | print(f"Additional info for this {ticker} ticker:")
64 | print(ticker_df.iloc[0])
65 |
66 |
67 | if __name__ == "__main__":
68 | main()
69 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv*
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 | tests/pytest.ini
116 |
117 | # Spyder project settings
118 | .spyderproject
119 | .spyproject
120 |
121 | # Rope project settings
122 | .ropeproject
123 |
124 | # mkdocs documentation
125 | /site
126 |
127 | # mypy
128 | .mypy_cache/
129 | .dmypy.json
130 | dmypy.json
131 |
132 | # Pyre type checker
133 | .pyre/
134 |
135 | # pytype static type analyzer
136 | .pytype/
137 |
138 | # Cython debug symbols
139 | cython_debug/
140 |
141 | .env
142 |
143 | docs/README.md
144 | docs/CHANGELOG.md
145 | docs/CONTRIBUTING.md
146 |
147 | .idea
148 | .market_data_cache
149 | .DS_Store
150 |
--------------------------------------------------------------------------------
/tinkoff/invest/caching/instruments_cache/instrument_storage.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import replace
3 | from typing import Dict, Generic, Tuple, TypeVar, cast
4 |
5 | from tinkoff.invest import InstrumentIdType
6 | from tinkoff.invest.caching.instruments_cache.models import (
7 | InstrumentResponse,
8 | InstrumentsResponse,
9 | )
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | TInstrumentResponse = TypeVar("TInstrumentResponse", bound=InstrumentResponse)
15 | TInstrumentsResponse = TypeVar("TInstrumentsResponse", bound=InstrumentsResponse)
16 |
17 |
18 | class InstrumentStorage(Generic[TInstrumentResponse, TInstrumentsResponse]):
19 | def __init__(self, instruments_response: TInstrumentsResponse):
20 | self._instruments_response = instruments_response
21 |
22 | self._instrument_by_class_code_figi: Dict[
23 | Tuple[str, str], InstrumentResponse
24 | ] = {
25 | (instrument.class_code, instrument.figi): instrument
26 | for instrument in self._instruments_response.instruments
27 | }
28 | self._instrument_by_class_code_ticker: Dict[
29 | Tuple[str, str], InstrumentResponse
30 | ] = {
31 | (instrument.class_code, instrument.ticker): instrument
32 | for instrument in self._instruments_response.instruments
33 | }
34 | self._instrument_by_class_code_uid: Dict[
35 | Tuple[str, str], InstrumentResponse
36 | ] = {
37 | (instrument.class_code, instrument.uid): instrument
38 | for instrument in self._instruments_response.instruments
39 | }
40 |
41 | # fmt: off
42 | self._instrument_by_class_code_id_index = {
43 | InstrumentIdType.INSTRUMENT_ID_UNSPECIFIED:
44 | self._instrument_by_class_code_figi,
45 | InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI:
46 | self._instrument_by_class_code_figi,
47 | InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER:
48 | self._instrument_by_class_code_ticker,
49 | InstrumentIdType.INSTRUMENT_ID_TYPE_UID:
50 | self._instrument_by_class_code_uid,
51 | }
52 | # fmt: on
53 |
54 | def get(
55 | self, *, id_type: InstrumentIdType, class_code: str, id: str
56 | ) -> TInstrumentResponse:
57 | logger.debug(
58 | "Cache request id_type=%s, class_code=%s, id=%s", id_type, class_code, id
59 | )
60 | instrument_by_class_code_id = self._instrument_by_class_code_id_index[id_type]
61 | logger.debug(
62 | "Index for %s found: \n%s", id_type, instrument_by_class_code_id.keys()
63 | )
64 | key = (class_code, id)
65 | logger.debug("Cache request key=%s", key)
66 |
67 | return cast(TInstrumentResponse, instrument_by_class_code_id[key])
68 |
69 | def get_instruments_response(self) -> TInstrumentsResponse:
70 | return replace(self._instruments_response, **{})
71 |
--------------------------------------------------------------------------------
/tests/test_marketdata.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=redefined-outer-name,unused-variable
2 | # pylint: disable=protected-access
3 | from unittest import mock
4 |
5 | import pytest
6 | from google.protobuf.json_format import MessageToDict
7 |
8 | from tinkoff.invest._grpc_helpers import dataclass_to_protobuff
9 | from tinkoff.invest.grpc import marketdata_pb2
10 | from tinkoff.invest.schemas import (
11 | GetMySubscriptions,
12 | MarketDataRequest,
13 | SubscribeTradesRequest,
14 | SubscriptionAction,
15 | TradeInstrument,
16 | )
17 | from tinkoff.invest.services import MarketDataService
18 |
19 |
20 | @pytest.fixture()
21 | def market_data_service():
22 | return mock.create_autospec(spec=MarketDataService)
23 |
24 |
25 | def test_get_candles(market_data_service):
26 | response = market_data_service.get_candles( # noqa: F841
27 | figi=mock.Mock(),
28 | from_=mock.Mock(),
29 | to=mock.Mock(),
30 | interval=mock.Mock(),
31 | )
32 | market_data_service.get_candles.assert_called_once()
33 |
34 |
35 | def test_get_last_prices(market_data_service):
36 | response = market_data_service.get_last_prices(figi=mock.Mock()) # noqa: F841
37 | market_data_service.get_last_prices.assert_called_once()
38 |
39 |
40 | def test_get_order_book(market_data_service):
41 | response = market_data_service.get_order_book( # noqa: F841
42 | figi=mock.Mock(), depth=mock.Mock()
43 | )
44 | market_data_service.get_order_book.assert_called_once()
45 |
46 |
47 | def test_get_trading_status(market_data_service):
48 | response = market_data_service.get_trading_status(figi=mock.Mock()) # noqa: F841
49 | market_data_service.get_trading_status.assert_called_once()
50 |
51 |
52 | def test_subscribe_trades_request():
53 | expected = marketdata_pb2.MarketDataRequest(
54 | subscribe_trades_request=marketdata_pb2.SubscribeTradesRequest(
55 | instruments=[marketdata_pb2.TradeInstrument(figi="figi")],
56 | subscription_action=SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE,
57 | with_open_interest=True,
58 | )
59 | )
60 |
61 | result = dataclass_to_protobuff(
62 | MarketDataRequest(
63 | subscribe_trades_request=SubscribeTradesRequest(
64 | instruments=[TradeInstrument(figi="figi")],
65 | subscription_action=SubscriptionAction.SUBSCRIPTION_ACTION_SUBSCRIBE,
66 | with_open_interest=True,
67 | )
68 | ),
69 | marketdata_pb2.MarketDataRequest(),
70 | )
71 |
72 | assert MessageToDict(result) == MessageToDict(expected)
73 |
74 |
75 | def test_market_data_request_get_my_subscriptions():
76 | expected = marketdata_pb2.MarketDataRequest(
77 | get_my_subscriptions=marketdata_pb2.GetMySubscriptions()
78 | )
79 |
80 | result = dataclass_to_protobuff(
81 | MarketDataRequest(get_my_subscriptions=GetMySubscriptions()),
82 | marketdata_pb2.MarketDataRequest(),
83 | )
84 |
85 | assert MessageToDict(result) == MessageToDict(expected)
86 |
--------------------------------------------------------------------------------
/tests/test_orders_canceling/test_orders_canceler.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import List
3 | from unittest.mock import call
4 |
5 | import pytest
6 |
7 | from tinkoff.invest import (
8 | GetOrdersResponse,
9 | GetStopOrdersResponse,
10 | OrderState,
11 | StopOrder,
12 | )
13 | from tinkoff.invest.services import OrdersService, Services, StopOrdersService
14 | from tinkoff.invest.typedefs import AccountId
15 |
16 |
17 | @pytest.fixture()
18 | def orders_service(mocker) -> OrdersService:
19 | return mocker.create_autospec(OrdersService)
20 |
21 |
22 | @pytest.fixture()
23 | def stop_orders_service(mocker) -> StopOrdersService:
24 | return mocker.create_autospec(StopOrdersService)
25 |
26 |
27 | @pytest.fixture()
28 | def services(
29 | mocker, orders_service: OrdersService, stop_orders_service: StopOrdersService
30 | ) -> Services:
31 | services = mocker.create_autospec(Services)
32 | services.orders = orders_service
33 | services.stop_orders = stop_orders_service
34 | return services
35 |
36 |
37 | @pytest.fixture()
38 | def account_id() -> AccountId:
39 | return AccountId(uuid.uuid4().hex)
40 |
41 |
42 | class TestOrdersCanceler:
43 | @pytest.mark.parametrize(
44 | "orders",
45 | [
46 | [
47 | OrderState(order_id=str(uuid.uuid4())),
48 | OrderState(order_id=str(uuid.uuid4())),
49 | OrderState(order_id=str(uuid.uuid4())),
50 | ],
51 | [OrderState(order_id=str(uuid.uuid4()))],
52 | [],
53 | ],
54 | )
55 | @pytest.mark.parametrize(
56 | "stop_orders",
57 | [
58 | [
59 | StopOrder(stop_order_id=str(uuid.uuid4())),
60 | StopOrder(stop_order_id=str(uuid.uuid4())),
61 | StopOrder(stop_order_id=str(uuid.uuid4())),
62 | ],
63 | [
64 | StopOrder(stop_order_id=str(uuid.uuid4())),
65 | ],
66 | [],
67 | ],
68 | )
69 | def test_cancels_all_orders(
70 | self,
71 | services: Services,
72 | orders_service: OrdersService,
73 | stop_orders_service: StopOrdersService,
74 | account_id: AccountId,
75 | orders: List[OrderState],
76 | stop_orders: List[StopOrder],
77 | ):
78 | orders_service.get_orders.return_value = GetOrdersResponse(orders=orders)
79 | stop_orders_service.get_stop_orders.return_value = GetStopOrdersResponse(
80 | stop_orders=stop_orders
81 | )
82 |
83 | Services.cancel_all_orders(services, account_id=account_id)
84 |
85 | orders_service.get_orders.assert_called_once()
86 | orders_service.cancel_order.assert_has_calls(
87 | call(account_id=account_id, order_id=order.order_id) for order in orders
88 | )
89 | stop_orders_service.get_stop_orders.assert_called_once()
90 | stop_orders_service.cancel_stop_order.assert_has_calls(
91 | call(account_id=account_id, stop_order_id=stop_order.stop_order_id)
92 | for stop_order in stop_orders
93 | )
94 |
--------------------------------------------------------------------------------
/tinkoff/invest/market_data_stream/market_data_stream_manager.py:
--------------------------------------------------------------------------------
1 | import queue
2 | import threading
3 | from typing import Iterable, Iterator
4 |
5 | from tinkoff.invest.market_data_stream.market_data_stream_interface import (
6 | IMarketDataStreamManager,
7 | )
8 | from tinkoff.invest.market_data_stream.stream_managers import (
9 | CandlesStreamManager,
10 | InfoStreamManager,
11 | LastPriceStreamManager,
12 | OrderBookStreamManager,
13 | TradesStreamManager,
14 | )
15 | from tinkoff.invest.schemas import MarketDataRequest, MarketDataResponse
16 |
17 |
18 | class MarketDataStreamManager(IMarketDataStreamManager):
19 | def __init__(
20 | self,
21 | market_data_stream_service: ( # type: ignore
22 | "MarketDataStreamService" # noqa: F821
23 | ),
24 | ):
25 | self._market_data_stream_service = market_data_stream_service
26 | self._market_data_stream: Iterator[MarketDataResponse]
27 | self._requests: queue.Queue[MarketDataRequest] = queue.Queue()
28 | self._unsubscribe_event = threading.Event()
29 |
30 | def _get_request_generator(self) -> Iterable[MarketDataRequest]:
31 | while not self._unsubscribe_event.is_set() or not self._requests.empty():
32 | try:
33 | request = self._requests.get(timeout=1.0)
34 | except queue.Empty:
35 | pass
36 | else:
37 | yield request
38 |
39 | @property
40 | def candles(self) -> "CandlesStreamManager[MarketDataStreamManager]":
41 | return CandlesStreamManager[MarketDataStreamManager](parent_manager=self)
42 |
43 | @property
44 | def order_book(self) -> "OrderBookStreamManager[MarketDataStreamManager]":
45 | return OrderBookStreamManager[MarketDataStreamManager](parent_manager=self)
46 |
47 | @property
48 | def trades(self) -> "TradesStreamManager[MarketDataStreamManager]":
49 | return TradesStreamManager[MarketDataStreamManager](parent_manager=self)
50 |
51 | @property
52 | def info(self) -> "InfoStreamManager[MarketDataStreamManager]":
53 | return InfoStreamManager[MarketDataStreamManager](parent_manager=self)
54 |
55 | @property
56 | def last_price(self) -> "LastPriceStreamManager[MarketDataStreamManager]":
57 | return LastPriceStreamManager[MarketDataStreamManager](parent_manager=self)
58 |
59 | def subscribe(self, market_data_request: MarketDataRequest) -> None:
60 | self._requests.put(market_data_request)
61 |
62 | def unsubscribe(self, market_data_request: MarketDataRequest) -> None:
63 | self._requests.put(market_data_request)
64 |
65 | def stop(self) -> None:
66 | self._unsubscribe_event.set()
67 |
68 | def __iter__(self) -> "MarketDataStreamManager":
69 | self._unsubscribe_event.clear()
70 | self._market_data_stream = iter(
71 | self._market_data_stream_service.market_data_stream(
72 | self._get_request_generator()
73 | )
74 | )
75 | return self
76 |
77 | def __next__(self) -> MarketDataResponse:
78 | return next(self._market_data_stream)
79 |
--------------------------------------------------------------------------------
/examples/strategies/moving_average.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from datetime import timedelta
4 | from decimal import Decimal
5 |
6 | from matplotlib import pyplot as plt
7 |
8 | from tinkoff.invest import CandleInterval, Client
9 | from tinkoff.invest.strategies.base.account_manager import AccountManager
10 | from tinkoff.invest.strategies.moving_average.plotter import (
11 | MovingAverageStrategyPlotter,
12 | )
13 | from tinkoff.invest.strategies.moving_average.signal_executor import (
14 | MovingAverageSignalExecutor,
15 | )
16 | from tinkoff.invest.strategies.moving_average.strategy import MovingAverageStrategy
17 | from tinkoff.invest.strategies.moving_average.strategy_settings import (
18 | MovingAverageStrategySettings,
19 | )
20 | from tinkoff.invest.strategies.moving_average.strategy_state import (
21 | MovingAverageStrategyState,
22 | )
23 | from tinkoff.invest.strategies.moving_average.supervisor import (
24 | MovingAverageStrategySupervisor,
25 | )
26 | from tinkoff.invest.strategies.moving_average.trader import MovingAverageStrategyTrader
27 |
28 | logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.INFO)
29 | logger = logging.getLogger(__name__)
30 |
31 |
32 | TOKEN = os.environ["INVEST_TOKEN"]
33 | FIGI = os.environ["INVEST_FIGI"]
34 | ACCOUNT_ID = os.environ["INVEST_ACCOUNT_ID"]
35 |
36 |
37 | def main():
38 | with Client(TOKEN) as services:
39 | settings = MovingAverageStrategySettings(
40 | share_id=FIGI,
41 | account_id=ACCOUNT_ID,
42 | max_transaction_price=Decimal(10000),
43 | candle_interval=CandleInterval.CANDLE_INTERVAL_1_MIN,
44 | long_period=timedelta(minutes=100),
45 | short_period=timedelta(minutes=20),
46 | std_period=timedelta(minutes=30),
47 | )
48 |
49 | account_manager = AccountManager(services=services, strategy_settings=settings)
50 | state = MovingAverageStrategyState()
51 | strategy = MovingAverageStrategy(
52 | settings=settings,
53 | account_manager=account_manager,
54 | state=state,
55 | )
56 | signal_executor = MovingAverageSignalExecutor(
57 | services=services,
58 | state=state,
59 | settings=settings,
60 | )
61 | supervisor = MovingAverageStrategySupervisor()
62 | trader = MovingAverageStrategyTrader(
63 | strategy=strategy,
64 | settings=settings,
65 | services=services,
66 | state=state,
67 | signal_executor=signal_executor,
68 | account_manager=account_manager,
69 | supervisor=supervisor,
70 | )
71 | plotter = MovingAverageStrategyPlotter(settings=settings)
72 |
73 | initial_balance = account_manager.get_current_balance()
74 |
75 | for i in range(5):
76 | logger.info("Trade %s", i)
77 | trader.trade()
78 |
79 | current_balance = account_manager.get_current_balance()
80 |
81 | logger.info("Initial balance %s", initial_balance)
82 | logger.info("Current balance %s", current_balance)
83 |
84 | events = supervisor.get_events()
85 | plotter.plot(events)
86 | plt.show()
87 |
--------------------------------------------------------------------------------
/tinkoff/invest/market_data_stream/async_market_data_stream_manager.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import threading
3 | from asyncio import Queue
4 | from typing import AsyncIterable, AsyncIterator, Awaitable
5 |
6 | from tinkoff.invest.market_data_stream.market_data_stream_interface import (
7 | IMarketDataStreamManager,
8 | )
9 | from tinkoff.invest.market_data_stream.stream_managers import (
10 | CandlesStreamManager,
11 | InfoStreamManager,
12 | LastPriceStreamManager,
13 | OrderBookStreamManager,
14 | TradesStreamManager,
15 | )
16 | from tinkoff.invest.schemas import MarketDataRequest, MarketDataResponse
17 |
18 |
19 | class AsyncMarketDataStreamManager(IMarketDataStreamManager):
20 | def __init__(
21 | self,
22 | market_data_stream: "MarketDataStreamService", # type: ignore # noqa: F821
23 | ):
24 | self._market_data_stream_service = market_data_stream
25 | self._market_data_stream: AsyncIterator[MarketDataResponse]
26 | self._requests: Queue[MarketDataRequest] = Queue()
27 | self._unsubscribe_event = threading.Event()
28 |
29 | async def _get_request_generator(self) -> AsyncIterable[MarketDataRequest]:
30 | while not self._unsubscribe_event.is_set() or not self._requests.empty():
31 | try:
32 | request = await asyncio.wait_for(self._requests.get(), timeout=1.0)
33 | except asyncio.exceptions.TimeoutError:
34 | pass
35 | else:
36 | yield request
37 | self._requests.task_done()
38 |
39 | @property
40 | def candles(self) -> "CandlesStreamManager[AsyncMarketDataStreamManager]":
41 | return CandlesStreamManager[AsyncMarketDataStreamManager](parent_manager=self)
42 |
43 | @property
44 | def order_book(self) -> "OrderBookStreamManager[AsyncMarketDataStreamManager]":
45 | return OrderBookStreamManager[AsyncMarketDataStreamManager](parent_manager=self)
46 |
47 | @property
48 | def trades(self) -> "TradesStreamManager[AsyncMarketDataStreamManager]":
49 | return TradesStreamManager[AsyncMarketDataStreamManager](parent_manager=self)
50 |
51 | @property
52 | def info(self) -> "InfoStreamManager[AsyncMarketDataStreamManager]":
53 | return InfoStreamManager[AsyncMarketDataStreamManager](parent_manager=self)
54 |
55 | @property
56 | def last_price(self) -> "LastPriceStreamManager[AsyncMarketDataStreamManager]":
57 | return LastPriceStreamManager[AsyncMarketDataStreamManager](parent_manager=self)
58 |
59 | def subscribe(self, market_data_request: MarketDataRequest) -> None:
60 | self._requests.put_nowait(market_data_request)
61 |
62 | def unsubscribe(self, market_data_request: MarketDataRequest) -> None:
63 | self._requests.put_nowait(market_data_request)
64 |
65 | def stop(self) -> None:
66 | self._unsubscribe_event.set()
67 |
68 | def __aiter__(self) -> "AsyncMarketDataStreamManager":
69 | self._unsubscribe_event.clear()
70 | self._market_data_stream = self._market_data_stream_service.market_data_stream(
71 | self._get_request_generator()
72 | ).__aiter__()
73 |
74 | return self
75 |
76 | def __anext__(self) -> Awaitable[MarketDataResponse]:
77 | return self._market_data_stream.__anext__()
78 |
--------------------------------------------------------------------------------
/tests/test_orders_canceling/test_async_orders_canceler.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import List
3 | from unittest.mock import call
4 |
5 | import pytest
6 | import pytest_asyncio
7 |
8 | from tinkoff.invest import (
9 | GetOrdersResponse,
10 | GetStopOrdersResponse,
11 | OrderState,
12 | StopOrder,
13 | )
14 | from tinkoff.invest.async_services import (
15 | AsyncServices,
16 | OrdersService,
17 | StopOrdersService,
18 | )
19 | from tinkoff.invest.typedefs import AccountId
20 |
21 |
22 | @pytest_asyncio.fixture()
23 | async def orders_service(mocker) -> OrdersService:
24 | return mocker.create_autospec(OrdersService)
25 |
26 |
27 | @pytest_asyncio.fixture()
28 | async def stop_orders_service(mocker) -> StopOrdersService:
29 | return mocker.create_autospec(StopOrdersService)
30 |
31 |
32 | @pytest_asyncio.fixture()
33 | async def async_services(
34 | mocker, orders_service: OrdersService, stop_orders_service: StopOrdersService
35 | ) -> AsyncServices:
36 | async_services = mocker.create_autospec(AsyncServices)
37 | async_services.orders = orders_service
38 | async_services.stop_orders = stop_orders_service
39 | return async_services
40 |
41 |
42 | @pytest.fixture()
43 | def account_id() -> AccountId:
44 | return AccountId(uuid.uuid4().hex)
45 |
46 |
47 | class TestAsyncOrdersCanceling:
48 | @pytest.mark.asyncio
49 | @pytest.mark.parametrize(
50 | "orders",
51 | [
52 | [
53 | OrderState(order_id=str(uuid.uuid4())),
54 | OrderState(order_id=str(uuid.uuid4())),
55 | OrderState(order_id=str(uuid.uuid4())),
56 | ],
57 | [OrderState(order_id=str(uuid.uuid4()))],
58 | [],
59 | ],
60 | )
61 | @pytest.mark.parametrize(
62 | "stop_orders",
63 | [
64 | [
65 | StopOrder(stop_order_id=str(uuid.uuid4())),
66 | StopOrder(stop_order_id=str(uuid.uuid4())),
67 | StopOrder(stop_order_id=str(uuid.uuid4())),
68 | ],
69 | [
70 | StopOrder(stop_order_id=str(uuid.uuid4())),
71 | ],
72 | [],
73 | ],
74 | )
75 | async def test_cancels_all_orders(
76 | self,
77 | async_services: AsyncServices,
78 | orders_service: OrdersService,
79 | stop_orders_service: StopOrdersService,
80 | account_id: AccountId,
81 | orders: List[OrderState],
82 | stop_orders: List[StopOrder],
83 | ):
84 | orders_service.get_orders.return_value = GetOrdersResponse(orders=orders)
85 | stop_orders_service.get_stop_orders.return_value = GetStopOrdersResponse(
86 | stop_orders=stop_orders
87 | )
88 |
89 | await AsyncServices.cancel_all_orders(async_services, account_id=account_id)
90 |
91 | orders_service.get_orders.assert_called_once()
92 | orders_service.cancel_order.assert_has_calls(
93 | call(account_id=account_id, order_id=order.order_id) for order in orders
94 | )
95 | stop_orders_service.get_stop_orders.assert_called_once()
96 | stop_orders_service.cancel_stop_order.assert_has_calls(
97 | call(account_id=account_id, stop_order_id=stop_order.stop_order_id)
98 | for stop_order in stop_orders
99 | )
100 |
--------------------------------------------------------------------------------
/tinkoff/invest/clients.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 |
3 | import grpc
4 | from grpc.aio import ClientInterceptor
5 |
6 | from .async_services import AsyncServices
7 | from .channels import create_channel
8 | from .services import Services
9 | from .typedefs import ChannelArgumentType
10 |
11 | __all__ = ("Client", "AsyncClient")
12 |
13 |
14 | class Client:
15 | """Sync client.
16 |
17 | ```python
18 | import os
19 | from tinkoff.invest import Client
20 |
21 | TOKEN = os.environ["INVEST_TOKEN"]
22 |
23 | def main():
24 | with Client(TOKEN) as client:
25 | print(client.users.get_accounts())
26 |
27 | ```
28 | """
29 |
30 | def __init__(
31 | self,
32 | token: str,
33 | *,
34 | target: Optional[str] = None,
35 | sandbox_token: Optional[str] = None,
36 | options: Optional[ChannelArgumentType] = None,
37 | app_name: Optional[str] = None,
38 | interceptors: Optional[List[ClientInterceptor]] = None,
39 | ):
40 | self._token = token
41 | self._sandbox_token = sandbox_token
42 | self._options = options
43 | self._app_name = app_name
44 |
45 | self._channel = create_channel(target=target, options=options)
46 | if interceptors is None:
47 | interceptors = []
48 | for interceptor in interceptors:
49 | self._channel = grpc.intercept_channel(self._channel, interceptor)
50 |
51 | def __enter__(self) -> Services:
52 | channel = self._channel.__enter__()
53 | return Services(
54 | channel,
55 | token=self._token,
56 | sandbox_token=self._sandbox_token,
57 | app_name=self._app_name,
58 | )
59 |
60 | def __exit__(self, exc_type, exc_val, exc_tb):
61 | self._channel.__exit__(exc_type, exc_val, exc_tb)
62 | return False
63 |
64 |
65 | class AsyncClient:
66 | """Async client.
67 |
68 | ```python
69 | import asyncio
70 | import os
71 |
72 | from tinkoff.invest import AsyncClient
73 |
74 | TOKEN = os.environ["INVEST_TOKEN"]
75 |
76 |
77 | async def main():
78 | async with AsyncClient(TOKEN) as client:
79 | print(await client.users.get_accounts())
80 |
81 |
82 | if __name__ == "__main__":
83 | asyncio.run(main())
84 | ```
85 | """
86 |
87 | def __init__(
88 | self,
89 | token: str,
90 | *,
91 | target: Optional[str] = None,
92 | sandbox_token: Optional[str] = None,
93 | options: Optional[ChannelArgumentType] = None,
94 | app_name: Optional[str] = None,
95 | interceptors: Optional[List[ClientInterceptor]] = None,
96 | ):
97 | self._token = token
98 | self._sandbox_token = sandbox_token
99 | self._options = options
100 | self._app_name = app_name
101 | self._channel = create_channel(
102 | target=target, force_async=True, options=options, interceptors=interceptors
103 | )
104 |
105 | async def __aenter__(self) -> AsyncServices:
106 | channel = await self._channel.__aenter__()
107 | return AsyncServices(
108 | channel,
109 | token=self._token,
110 | sandbox_token=self._sandbox_token,
111 | app_name=self._app_name,
112 | )
113 |
114 | async def __aexit__(self, exc_type, exc_val, exc_tb):
115 | await self._channel.__aexit__(exc_type, exc_val, exc_tb)
116 | return False
117 |
--------------------------------------------------------------------------------
/examples/wiseplat_set_get_sandbox_balance.py:
--------------------------------------------------------------------------------
1 | """ Example - How to set/get balance for sandbox account.
2 | How to get/close all sandbox accounts.
3 | How to open new sandbox account. """
4 |
5 | import logging
6 | import os
7 | from datetime import datetime
8 | from decimal import Decimal
9 |
10 | from tinkoff.invest import MoneyValue
11 | from tinkoff.invest.sandbox.client import SandboxClient
12 | from tinkoff.invest.utils import decimal_to_quotation, quotation_to_decimal
13 |
14 | TOKEN = os.environ["INVEST_TOKEN"]
15 |
16 | logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG)
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | def add_money_sandbox(client, account_id, money, currency="rub"):
21 | """Function to add money to sandbox account."""
22 | money = decimal_to_quotation(Decimal(money))
23 | return client.sandbox.sandbox_pay_in(
24 | account_id=account_id,
25 | amount=MoneyValue(units=money.units, nano=money.nano, currency=currency),
26 | )
27 |
28 |
29 | def main():
30 | """Example - How to set/get balance for sandbox account.
31 | How to get/close all sandbox accounts.
32 | How to open new sandbox account."""
33 | with SandboxClient(TOKEN) as client:
34 | # get all sandbox accounts
35 | sandbox_accounts = client.users.get_accounts()
36 | print(sandbox_accounts)
37 |
38 | # close all sandbox accounts
39 | for sandbox_account in sandbox_accounts.accounts:
40 | client.sandbox.close_sandbox_account(account_id=sandbox_account.id)
41 |
42 | # open new sandbox account
43 | sandbox_account = client.sandbox.open_sandbox_account()
44 | print(sandbox_account.account_id)
45 |
46 | account_id = sandbox_account.account_id
47 |
48 | # add initial 2 000 000 to sandbox account
49 | print(add_money_sandbox(client=client, account_id=account_id, money=2000000))
50 | logger.info(
51 | "positions: %s", client.operations.get_positions(account_id=account_id)
52 | )
53 | print(
54 | "money: ",
55 | float(
56 | quotation_to_decimal(
57 | client.operations.get_positions(account_id=account_id).money[0]
58 | )
59 | ),
60 | )
61 |
62 | logger.info("orders: %s", client.orders.get_orders(account_id=account_id))
63 | logger.info(
64 | "positions: %s", client.operations.get_positions(account_id=account_id)
65 | )
66 | logger.info(
67 | "portfolio: %s", client.operations.get_portfolio(account_id=account_id)
68 | )
69 | logger.info(
70 | "operations: %s",
71 | client.operations.get_operations(
72 | account_id=account_id,
73 | from_=datetime(2023, 1, 1),
74 | to=datetime(2023, 2, 5),
75 | ),
76 | )
77 | logger.info(
78 | "withdraw_limits: %s",
79 | client.operations.get_withdraw_limits(account_id=account_id),
80 | )
81 |
82 | # add + 2 000 000 to sandbox account, total is 4 000 000
83 | print(add_money_sandbox(client=client, account_id=account_id, money=2000000))
84 | logger.info(
85 | "positions: %s", client.operations.get_positions(account_id=account_id)
86 | )
87 |
88 | # close new sandbox account
89 | sandbox_account = client.sandbox.close_sandbox_account(
90 | account_id=sandbox_account.account_id
91 | )
92 | print(sandbox_account)
93 |
94 |
95 | if __name__ == "__main__":
96 | main()
97 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["poetry-core>=1.0.0"]
3 | build-backend = "poetry.core.masonry.api"
4 |
5 | [tool.poetry]
6 | name = "tinkoff-investments"
7 | version = "0.2.0-beta117"
8 | description = "Tinkoff Python SDK"
9 | authors = ["Tinkoff Team "]
10 | license = "Apache-2.0"
11 | readme = "README.md"
12 | repository = "https://github.com/RussianInvestments/invest-python"
13 | homepage = "https://github.com/RussianInvestments/invest-python"
14 | packages = [
15 | {include = "tinkoff"}
16 | ]
17 | exclude = ["tinkoff/__init__.py"]
18 |
19 | [tool.poetry.dependencies]
20 | python = "^3.8.1"
21 | cachetools = "^5.2.0"
22 | grpcio = "^1.59.3"
23 | protobuf = "^4.25.1"
24 | python-dateutil = "^2.8.2"
25 | tinkoff = "^0.1.1"
26 | deprecation = "^2.1.0"
27 | matplotlib = {version = "^3.5.1", optional = true}
28 | mplfinance = {version = "^0.12.8-beta.9", optional = true}
29 | numpy = {version = "^1.22.2", optional = true}
30 | pandas = {version = ">=1.4.0", optional = true}
31 |
32 | [tool.poetry.extras]
33 | all = ["pandas", "numpy", "matplotlib", "mplfinance"]
34 |
35 | [tool.poetry.group.bump.dependencies]
36 | PyYAML = "^6.0"
37 | tomlkit = "^0.12.3"
38 |
39 | [tool.poetry.group.dev.dependencies]
40 | black = {extras = ["jupyter"], version = "^23.7.0"}
41 | codecov = "^2.1.12"
42 | grpcio-tools = "^1.59.3"
43 | ipython = "^8.1.1"
44 | isort = "^5.10.1"
45 | mypy = "^1.7.1"
46 | mypy-protobuf = "^3.5.0"
47 | pytest = "^7.4.3"
48 | pytest-asyncio = "^0.23.2"
49 | pytest-cov = "^4.1.0"
50 | pytest-deadfixtures = "^2.2.1"
51 | pytest-freezegun = "^0.4.2"
52 | pytest-mock = "^3.12.0"
53 | requests = "^2.27.1"
54 | ruff = "^0.1.6"
55 | types-cachetools = "^5.2.1"
56 | types-protobuf = "^4.23.0.4"
57 | types-python-dateutil = "^2.8.12"
58 | types-PyYAML = "^6.0.7"
59 | types-requests = "^2.27.7"
60 |
61 | [tool.poetry.group.docs.dependencies]
62 | mkdocs = "1.5.3"
63 | mkdocs-include-markdown-plugin = "^6.0.4"
64 | mkdocs-material = "^9.4.14"
65 | mkdocstrings = {version = "0.24.0", extras = ["python"]}
66 | termynal = "^0.11.1"
67 | griffe = "0.38.0"
68 |
69 | [tool.pytest.ini_options]
70 | testpaths = "tests"
71 | addopts = "--strict-markers --showlocals --verbosity 2"
72 | log_level = "DEBUG"
73 | asyncio_mode = "auto"
74 |
75 | [tool.ruff]
76 | line-length = 88
77 | select = [
78 | "D",
79 | "B",
80 | "C",
81 | "E",
82 | "F",
83 | "Q",
84 | "RUF001",
85 | "T",
86 | "W"
87 | ]
88 | ignore = [
89 | "D100",
90 | "D101",
91 | "D102",
92 | "D103",
93 | "D104",
94 | "D105",
95 | "D106",
96 | "D107",
97 | "D203",
98 | "D213",
99 | "B008",
100 | "B905",
101 | "Q000"
102 | ]
103 | exclude = [
104 | "tinkoff/invest/grpc",
105 | "examples/*"
106 | ]
107 |
108 | [tool.black]
109 | exclude = "tinkoff/invest/grpc"
110 |
111 | [tool.coverage.report]
112 | show_missing = true
113 | skip_covered = true
114 | fail_under = 64
115 | exclude_lines = [
116 | "raise NotImplementedError",
117 | "def __repr__",
118 | "pragma: no cover"
119 | ]
120 | omit = [
121 | "*/.local/*",
122 | "tests/*",
123 | "**/__main__.py"
124 | ]
125 | branch = true
126 | source = "tinkoff"
127 |
128 | [tool.isort]
129 | profile = "black"
130 | multi_line_output = 3
131 | combine_as_imports = true
132 |
133 | [tool.mypy]
134 | ignore_missing_imports = true
135 | no_implicit_optional = true
136 | check_untyped_defs = true
137 | exclude = ['venv', '.venv']
138 |
139 | [[tool.mypy.overrides]]
140 | module = ["tests.*", "examples.*"]
141 | check_untyped_defs = false
142 |
143 | [[tool.mypy.overrides]]
144 | module = ["tinkoff.invest.caching.instruments_cache.*", "tinkoff.invest.mock_services.*"]
145 | ignore_errors = true
146 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue.yaml:
--------------------------------------------------------------------------------
1 | name: Custom Issue
2 | description: Проблемы, вопросы, ...
3 | body:
4 | - type: textarea
5 | id: what-happened
6 | attributes:
7 | label: Что случилось?
8 | description: Краткое описание.
9 | validations:
10 | required: true
11 | - type: textarea
12 | id: to-reproduce
13 | attributes:
14 | label: Воспроизведение
15 | description: Код повторяющий кейс
16 | render: Python
17 | validations:
18 | required: false
19 | - type: dropdown
20 | id: package-version
21 | attributes:
22 | label: Tinkoff Invest Version
23 | description: Какая версия библиотеки используется?
24 | options:
25 | - 0.2.0-beta117
26 | - 0.2.0-beta116
27 | - 0.2.0-beta115
28 | - 0.2.0-beta114
29 | - 0.2.0-beta113
30 | - 0.2.0-beta112
31 | - 0.2.0-beta111
32 | - 0.2.0-beta110
33 | - 0.2.0-beta109
34 | - 0.2.0-beta108
35 | - 0.2.0-beta107
36 | - 0.2.0-beta106
37 | - 0.2.0-beta105
38 | - 0.2.0-beta104
39 | - 0.2.0-beta103
40 | - 0.2.0-beta101
41 | - 0.2.0-beta100
42 | - 0.2.0-beta99
43 | - 0.2.0-beta98
44 | - 0.2.0-beta97
45 | - 0.2.0-beta96
46 | - 0.2.0-beta95
47 | - 0.2.0-beta94
48 | - 0.2.0-beta93
49 | - 0.2.0-beta92
50 | - 0.2.0-beta91
51 | - 0.2.0-beta90
52 | - 0.2.0-beta89
53 | - 0.2.0-beta88
54 | - 0.2.0-beta87
55 | - 0.2.0-beta86
56 | - 0.2.0-beta85
57 | - 0.2.0-beta84
58 | - 0.2.0-beta83
59 | - 0.2.0-beta82
60 | - 0.2.0-beta81
61 | - 0.2.0-beta80
62 | - 0.2.0-beta79
63 | - 0.2.0-beta78
64 | - 0.2.0-beta77
65 | - 0.2.0-beta76
66 | - 0.2.0-beta75
67 | - 0.2.0-beta74
68 | - 0.2.0-beta73
69 | - 0.2.0-beta72
70 | - 0.2.0-beta71
71 | - 0.2.0-beta70
72 | - 0.2.0-beta69
73 | - 0.2.0-beta68
74 | - 0.2.0-beta67
75 | - 0.2.0-beta66
76 | - 0.2.0-beta65
77 | - 0.2.0-beta64
78 | - 0.2.0-beta63
79 | - 0.2.0-beta62
80 | - 0.2.0-beta61
81 | - 0.2.0-beta60
82 | - 0.2.0-beta59
83 | - 0.2.0-beta58
84 | - 0.2.0-beta57
85 | - 0.2.0-beta56
86 | - 0.2.0-beta55
87 | - 0.2.0-beta54
88 | - 0.2.0-beta53
89 | - 0.2.0-beta52
90 | - 0.2.0-beta51
91 | - 0.2.0-beta50
92 | - 0.2.0-beta49
93 | - 0.2.0-beta48
94 | - 0.2.0-beta47
95 | - 0.2.0-beta46
96 | - 0.2.0-beta45
97 | - 0.2.0-beta44
98 | - 0.2.0-beta43
99 | - 0.2.0-beta42
100 | - 0.2.0-beta41
101 | - 0.2.0-beta40
102 | - 0.2.0-beta39
103 | - 0.2.0-beta38
104 | - 0.2.0-beta37
105 | - 0.2.0-beta36
106 | - 0.2.0-beta35
107 | - 0.2.0-beta34
108 | - 0.2.0-beta33
109 | - 0.2.0-beta32
110 | - 0.2.0-beta31
111 | - 0.2.0-beta30
112 | - 0.2.0-beta29
113 | - 0.2.0-beta28
114 | - 0.2.0-beta27
115 | - Другая
116 | validations:
117 | required: true
118 | - type: dropdown
119 | id: python-version
120 | attributes:
121 | label: Python Version
122 | description: Какая версия Python-а используется?
123 | options:
124 | - '3.11'
125 | - '3.10'
126 | - '3.9'
127 | - '3.8'
128 | - Другая
129 | validations:
130 | required: true
131 | - type: dropdown
132 | id: os
133 | attributes:
134 | label: OS
135 | description: Ваша операционная система.
136 | options:
137 | - Windows
138 | - Linux
139 | - Mac OS
140 | - Mac OS (m1)
141 | - Другая
142 | validations:
143 | required: true
144 | - type: textarea
145 | id: logs
146 | attributes:
147 | label: Логи
148 | description: Скопируйте и вставьте сюда логи. Не забудьте скрыть чувствительные
149 | данные.
150 | render: Shell
151 |
--------------------------------------------------------------------------------
/examples/wiseplat_create_take_profit_stop_order.py:
--------------------------------------------------------------------------------
1 | """Example - How to create takeprofit buy order."""
2 | import logging
3 | import os
4 | from decimal import Decimal
5 |
6 | from tinkoff.invest import (
7 | Client,
8 | InstrumentIdType,
9 | StopOrderDirection,
10 | StopOrderExpirationType,
11 | StopOrderType,
12 | )
13 | from tinkoff.invest.exceptions import InvestError
14 | from tinkoff.invest.utils import decimal_to_quotation, quotation_to_decimal
15 |
16 | TOKEN = os.environ["INVEST_TOKEN"]
17 |
18 | logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG)
19 | logger = logging.getLogger(__name__)
20 |
21 |
22 | def main():
23 | """Example - How to create takeprofit buy order."""
24 | with Client(TOKEN) as client:
25 | response = client.users.get_accounts()
26 | account, *_ = response.accounts
27 | account_id = account.id
28 | logger.info("Orders: %s", client.orders.get_orders(account_id=account_id))
29 |
30 | figi = "BBG004730ZJ9" # BBG004730ZJ9 - VTBR / BBG004730N88 - SBER
31 |
32 | # getting the last price for instrument
33 | last_price = (
34 | client.market_data.get_last_prices(figi=[figi]).last_prices[0].price
35 | )
36 | last_price = quotation_to_decimal(last_price)
37 | print(f"figi, last price = {last_price}")
38 |
39 | # setting the percentage by which the takeprofit stop order
40 | # should be set below the last price
41 | percent_down = 5
42 |
43 | # calculation of the price for takeprofit stop order
44 | calculated_price = last_price - last_price * Decimal(percent_down / 100)
45 | print(f"calculated_price = {calculated_price}")
46 |
47 | # getting the min price increment and the number of digits after point
48 | min_price_increment = client.instruments.get_instrument_by(
49 | id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI, id=figi
50 | ).instrument.min_price_increment
51 | number_digits_after_point = 9 - len(str(min_price_increment.nano)) + 1
52 | min_price_increment = quotation_to_decimal(min_price_increment)
53 | print(
54 | f"min_price_increment = {min_price_increment}, "
55 | f"number_digits_after_point={number_digits_after_point}"
56 | )
57 |
58 | # calculation of the price for instrument which is
59 | # divisible to min price increment
60 | calculated_price = (
61 | round(calculated_price / min_price_increment) * min_price_increment
62 | )
63 | print(
64 | f"let's send stop order at price = "
65 | f"{calculated_price:.{number_digits_after_point}f} divisible to "
66 | f"min price increment {min_price_increment}"
67 | )
68 |
69 | # creating takeprofit buy order
70 | stop_order_type = StopOrderType.STOP_ORDER_TYPE_TAKE_PROFIT
71 | direction = StopOrderDirection.STOP_ORDER_DIRECTION_BUY
72 | exp_type = StopOrderExpirationType.STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL
73 | try:
74 | response = client.stop_orders.post_stop_order(
75 | figi=figi,
76 | quantity=1,
77 | price=decimal_to_quotation(Decimal(calculated_price)),
78 | stop_price=decimal_to_quotation(Decimal(calculated_price)),
79 | direction=direction,
80 | account_id=account_id,
81 | expiration_type=exp_type,
82 | stop_order_type=stop_order_type,
83 | expire_date=None,
84 | )
85 | print(response)
86 | print("stop_order_id=", response.stop_order_id)
87 | except InvestError as error:
88 | logger.error("Posting trade takeprofit order failed. Exception: %s", error)
89 |
90 |
91 | if __name__ == "__main__":
92 | main()
93 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Сообщить об ошибке
3 | title: '[Bug] Title'
4 | labels:
5 | - bug
6 | assignees:
7 | - daxartio
8 | body:
9 | - type: markdown
10 | attributes:
11 | value: 'Спасибо, что нашли время заполнить этот отчет об ошибке!
12 |
13 | '
14 | - type: textarea
15 | id: what-happened
16 | attributes:
17 | label: Что случилось?
18 | description: Краткое описание.
19 | validations:
20 | required: true
21 | - type: textarea
22 | id: to-reproduce
23 | attributes:
24 | label: Воспроизведение
25 | description: Код повторяющий кейс
26 | render: Python
27 | validations:
28 | required: false
29 | - type: dropdown
30 | id: package-version
31 | attributes:
32 | label: Tinkoff Invest Version
33 | description: Какая версия библиотеки используется?
34 | options:
35 | - 0.2.0-beta117
36 | - 0.2.0-beta116
37 | - 0.2.0-beta115
38 | - 0.2.0-beta114
39 | - 0.2.0-beta113
40 | - 0.2.0-beta112
41 | - 0.2.0-beta111
42 | - 0.2.0-beta110
43 | - 0.2.0-beta109
44 | - 0.2.0-beta108
45 | - 0.2.0-beta107
46 | - 0.2.0-beta106
47 | - 0.2.0-beta105
48 | - 0.2.0-beta104
49 | - 0.2.0-beta103
50 | - 0.2.0-beta101
51 | - 0.2.0-beta100
52 | - 0.2.0-beta99
53 | - 0.2.0-beta98
54 | - 0.2.0-beta97
55 | - 0.2.0-beta96
56 | - 0.2.0-beta95
57 | - 0.2.0-beta94
58 | - 0.2.0-beta93
59 | - 0.2.0-beta92
60 | - 0.2.0-beta91
61 | - 0.2.0-beta90
62 | - 0.2.0-beta89
63 | - 0.2.0-beta88
64 | - 0.2.0-beta87
65 | - 0.2.0-beta86
66 | - 0.2.0-beta85
67 | - 0.2.0-beta84
68 | - 0.2.0-beta83
69 | - 0.2.0-beta82
70 | - 0.2.0-beta81
71 | - 0.2.0-beta80
72 | - 0.2.0-beta79
73 | - 0.2.0-beta78
74 | - 0.2.0-beta77
75 | - 0.2.0-beta76
76 | - 0.2.0-beta75
77 | - 0.2.0-beta74
78 | - 0.2.0-beta73
79 | - 0.2.0-beta72
80 | - 0.2.0-beta71
81 | - 0.2.0-beta70
82 | - 0.2.0-beta69
83 | - 0.2.0-beta68
84 | - 0.2.0-beta67
85 | - 0.2.0-beta66
86 | - 0.2.0-beta65
87 | - 0.2.0-beta64
88 | - 0.2.0-beta63
89 | - 0.2.0-beta62
90 | - 0.2.0-beta61
91 | - 0.2.0-beta60
92 | - 0.2.0-beta59
93 | - 0.2.0-beta58
94 | - 0.2.0-beta57
95 | - 0.2.0-beta56
96 | - 0.2.0-beta55
97 | - 0.2.0-beta54
98 | - 0.2.0-beta53
99 | - 0.2.0-beta52
100 | - 0.2.0-beta51
101 | - 0.2.0-beta50
102 | - 0.2.0-beta49
103 | - 0.2.0-beta48
104 | - 0.2.0-beta47
105 | - 0.2.0-beta46
106 | - 0.2.0-beta45
107 | - 0.2.0-beta44
108 | - 0.2.0-beta43
109 | - 0.2.0-beta42
110 | - 0.2.0-beta41
111 | - 0.2.0-beta40
112 | - 0.2.0-beta39
113 | - 0.2.0-beta38
114 | - 0.2.0-beta37
115 | - 0.2.0-beta36
116 | - 0.2.0-beta35
117 | - 0.2.0-beta34
118 | - 0.2.0-beta33
119 | - 0.2.0-beta32
120 | - 0.2.0-beta31
121 | - 0.2.0-beta30
122 | - 0.2.0-beta29
123 | - 0.2.0-beta28
124 | - 0.2.0-beta27
125 | - Другая
126 | validations:
127 | required: true
128 | - type: dropdown
129 | id: python-version
130 | attributes:
131 | label: Python Version
132 | description: Какая версия Python-а используется?
133 | options:
134 | - '3.11'
135 | - '3.10'
136 | - '3.9'
137 | - '3.8'
138 | - Другая
139 | validations:
140 | required: true
141 | - type: dropdown
142 | id: os
143 | attributes:
144 | label: OS
145 | description: Ваша операционная система.
146 | options:
147 | - Windows
148 | - Linux
149 | - Mac OS
150 | - Mac OS (m1)
151 | - Другая
152 | validations:
153 | required: true
154 | - type: textarea
155 | id: logs
156 | attributes:
157 | label: Логи
158 | description: Скопируйте и вставьте сюда логи. Не забудьте скрыть чувствительные
159 | данные.
160 | render: Shell
161 |
--------------------------------------------------------------------------------