├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── feature_request.yaml │ └── issue.yaml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── bumpversion.yml │ ├── check.yml │ ├── check_pr_title.yml │ ├── github_pages.yml │ └── pypi.yml ├── .gitignore ├── BREAKING_CHANGES.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── conftest.py ├── docs ├── api │ └── clients.md ├── examples.md └── robots.md ├── examples ├── README.md ├── __init__.py ├── all_candles.py ├── async_all_candles.py ├── async_client.py ├── async_get_asset_reports.py ├── async_get_assets.py ├── async_get_bond_events.py ├── async_get_bonds.py ├── async_get_consensus_forecasts.py ├── async_get_forecast_by.py ├── async_get_last_prices.py ├── async_get_risk_rates.py ├── async_get_sandbox_max_lots.py ├── async_get_signals.py ├── async_get_strategies.py ├── async_get_tech_analysis.py ├── async_get_trading_statuses.py ├── async_indicatives.py ├── async_instrument_favorites.py ├── async_order_state_stream.py ├── async_post_order_async.py ├── async_retrying_client.py ├── async_stream_client.py ├── cancel_orders.py ├── client.py ├── download_all_candles.py ├── easy_async_stream_client.py ├── easy_stream_client.py ├── get_asset_fundamentals.py ├── get_asset_reports.py ├── get_assets.py ├── get_bond_events.py ├── get_bonds.py ├── get_brands.py ├── get_consensus_forecasts.py ├── get_forecast_by.py ├── get_last_prices.py ├── get_last_trades.py ├── get_operations_by_cursor.py ├── get_risk_rates.py ├── get_sandbox_max_lots.py ├── get_signals.py ├── get_strategies.py ├── get_tech_analysis.py ├── get_trading_statuses.py ├── get_user_info.py ├── indicatives.py ├── instrument_cache.py ├── instrument_favorites.py ├── instrument_find_by_ticker.py ├── instruments.py ├── logger.py ├── market_order_stop_order.py ├── max_lots.py ├── open_sandbox_account.py ├── options.py ├── order_price.py ├── order_state_stream.py ├── porfolio_stream_client.py ├── positions_stream.py ├── post_order.py ├── post_order_async.py ├── retrying_client.py ├── sandbox_client.py ├── strategies │ ├── moving_average.py │ ├── param-search.ipynb │ └── real-time-render.ipynb ├── stream_client.py ├── trailing_stop.py ├── wiseplat_cancel_all_stop_orders.py ├── wiseplat_create_take_profit_stop_order.py ├── wiseplat_get_figi_for_ticker.py ├── wiseplat_live_strategy_print_ohlcv.py └── wiseplat_set_get_sandbox_balance.py ├── mkdocs.yml ├── poetry.lock ├── protos └── tinkoff │ └── invest │ └── grpc │ ├── common.proto │ ├── google │ └── api │ │ └── field_behavior.proto │ ├── instruments.proto │ ├── marketdata.proto │ ├── operations.proto │ ├── orders.proto │ ├── sandbox.proto │ ├── signals.proto │ ├── stoporders.proto │ └── users.proto ├── pyproject.toml ├── pytest.ini ├── scripts ├── __init__.py ├── download_protos.py ├── update_issue_templates.py ├── update_package_version.py └── version.py ├── tests ├── __init__.py ├── caches │ ├── __init__.py │ ├── test_instrument_cache.py │ └── test_ttl_cache.py ├── data_loaders │ ├── __init__.py │ ├── test_cached_load.py │ ├── test_get_all_candles.py │ ├── test_round_datetime_range.py │ └── test_sandbox_cached_load.py ├── marketdata │ ├── __init__.py │ └── test_async_marketdata.py ├── test_datetime_utils.py ├── test_instruments.py ├── test_marketdata.py ├── test_operations.py ├── test_orders.py ├── test_orders_canceling │ ├── __init__.py │ ├── test_async_orders_canceler.py │ └── test_orders_canceler.py ├── test_protobuf_to_dataclass.py ├── test_quotation_convert.py ├── test_sandbox.py ├── test_signals.py ├── test_stoporders.py ├── test_strategies │ ├── __init__.py │ └── test_moving_average │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_trader.py │ │ ├── test_trader_in_sandbox.py │ │ └── test_trader_on_real_market_data.py ├── test_users.py ├── test_utils.py └── utils.py └── tinkoff ├── __init__.py └── invest ├── __init__.py ├── _errors.py ├── _grpc_helpers.py ├── async_services.py ├── caching ├── __init__.py ├── instruments_cache │ ├── __init__.py │ ├── instrument_storage.py │ ├── instruments_cache.py │ ├── interface.py │ ├── models.py │ ├── protocol.py │ └── settings.py ├── market_data_cache │ ├── __init__.py │ ├── cache.py │ ├── cache_settings.py │ ├── datetime_range.py │ ├── instrument_date_range_market_data.py │ ├── instrument_market_data_storage.py │ ├── interface.py │ └── serialization.py └── overrides.py ├── candle_getter_protocol.py ├── channels.py ├── clients.py ├── constants.py ├── exceptions.py ├── grpc ├── __init__.py ├── common_pb2.py ├── common_pb2.pyi ├── common_pb2_grpc.py ├── google │ └── api │ │ ├── field_behavior_pb2.py │ │ ├── field_behavior_pb2.pyi │ │ └── field_behavior_pb2_grpc.py ├── instruments_pb2.py ├── instruments_pb2.pyi ├── instruments_pb2_grpc.py ├── marketdata_pb2.py ├── marketdata_pb2.pyi ├── marketdata_pb2_grpc.py ├── operations_pb2.py ├── operations_pb2.pyi ├── operations_pb2_grpc.py ├── orders_pb2.py ├── orders_pb2.pyi ├── orders_pb2_grpc.py ├── sandbox_pb2.py ├── sandbox_pb2.pyi ├── sandbox_pb2_grpc.py ├── signals_pb2.py ├── signals_pb2.pyi ├── signals_pb2_grpc.py ├── stoporders_pb2.py ├── stoporders_pb2.pyi ├── stoporders_pb2_grpc.py ├── users_pb2.py ├── users_pb2.pyi └── users_pb2_grpc.py ├── logging.py ├── market_data_stream ├── __init__.py ├── async_market_data_stream_manager.py ├── market_data_stream_interface.py ├── market_data_stream_manager.py ├── stream_managers.py └── typevars.py ├── metadata.py ├── mock_services.py ├── py.typed ├── retrying ├── __init__.py ├── aio │ ├── __init__.py │ ├── client.py │ ├── grpc_interceptor.py │ └── retry_manager.py ├── base_retry_manager.py ├── settings.py ├── settings_protocol.py └── sync │ ├── __init__.py │ ├── client.py │ ├── grpc_interceptor.py │ └── retry_manager.py ├── sandbox ├── __init__.py ├── async_client.py └── client.py ├── schemas.py ├── services.py ├── strategies ├── __init__.py ├── base │ ├── __init__.py │ ├── account_manager.py │ ├── errors.py │ ├── event.py │ ├── models.py │ ├── signal.py │ ├── signal_executor_base.py │ ├── strategy_interface.py │ ├── strategy_settings_base.py │ ├── strategy_supervisor.py │ ├── trader_base.py │ └── trader_interface.py ├── moving_average │ ├── __init__.py │ ├── plotter.py │ ├── signal_executor.py │ ├── strategy.py │ ├── strategy_settings.py │ ├── strategy_state.py │ ├── supervisor.py │ └── trader.py └── plotting │ ├── __init__.py │ └── plotter.py ├── typedefs.py └── utils.py /.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-beta114 36 | - 0.2.0-beta113 37 | - 0.2.0-beta112 38 | - 0.2.0-beta111 39 | - 0.2.0-beta110 40 | - 0.2.0-beta109 41 | - 0.2.0-beta108 42 | - 0.2.0-beta107 43 | - 0.2.0-beta106 44 | - 0.2.0-beta105 45 | - 0.2.0-beta104 46 | - 0.2.0-beta103 47 | - 0.2.0-beta101 48 | - 0.2.0-beta100 49 | - 0.2.0-beta99 50 | - 0.2.0-beta98 51 | - 0.2.0-beta97 52 | - 0.2.0-beta96 53 | - 0.2.0-beta95 54 | - 0.2.0-beta94 55 | - 0.2.0-beta93 56 | - 0.2.0-beta92 57 | - 0.2.0-beta91 58 | - 0.2.0-beta90 59 | - 0.2.0-beta89 60 | - 0.2.0-beta88 61 | - 0.2.0-beta87 62 | - 0.2.0-beta86 63 | - 0.2.0-beta85 64 | - 0.2.0-beta84 65 | - 0.2.0-beta83 66 | - 0.2.0-beta82 67 | - 0.2.0-beta81 68 | - 0.2.0-beta80 69 | - 0.2.0-beta79 70 | - 0.2.0-beta78 71 | - 0.2.0-beta77 72 | - 0.2.0-beta76 73 | - 0.2.0-beta75 74 | - 0.2.0-beta74 75 | - 0.2.0-beta73 76 | - 0.2.0-beta72 77 | - 0.2.0-beta71 78 | - 0.2.0-beta70 79 | - 0.2.0-beta69 80 | - 0.2.0-beta68 81 | - 0.2.0-beta67 82 | - 0.2.0-beta66 83 | - 0.2.0-beta65 84 | - 0.2.0-beta64 85 | - 0.2.0-beta63 86 | - 0.2.0-beta62 87 | - 0.2.0-beta61 88 | - 0.2.0-beta60 89 | - 0.2.0-beta59 90 | - 0.2.0-beta58 91 | - 0.2.0-beta57 92 | - 0.2.0-beta56 93 | - 0.2.0-beta55 94 | - 0.2.0-beta54 95 | - 0.2.0-beta53 96 | - 0.2.0-beta52 97 | - 0.2.0-beta51 98 | - 0.2.0-beta50 99 | - 0.2.0-beta49 100 | - 0.2.0-beta48 101 | - 0.2.0-beta47 102 | - 0.2.0-beta46 103 | - 0.2.0-beta45 104 | - 0.2.0-beta44 105 | - 0.2.0-beta43 106 | - 0.2.0-beta42 107 | - 0.2.0-beta41 108 | - 0.2.0-beta40 109 | - 0.2.0-beta39 110 | - 0.2.0-beta38 111 | - 0.2.0-beta37 112 | - 0.2.0-beta36 113 | - 0.2.0-beta35 114 | - 0.2.0-beta34 115 | - 0.2.0-beta33 116 | - 0.2.0-beta32 117 | - 0.2.0-beta31 118 | - 0.2.0-beta30 119 | - 0.2.0-beta29 120 | - 0.2.0-beta28 121 | - 0.2.0-beta27 122 | - Другая 123 | validations: 124 | required: true 125 | - type: dropdown 126 | id: python-version 127 | attributes: 128 | label: Python Version 129 | description: Какая версия Python-а используется? 130 | options: 131 | - '3.11' 132 | - '3.10' 133 | - '3.9' 134 | - '3.8' 135 | - Другая 136 | validations: 137 | required: true 138 | - type: dropdown 139 | id: os 140 | attributes: 141 | label: OS 142 | description: Ваша операционная система. 143 | options: 144 | - Windows 145 | - Linux 146 | - Mac OS 147 | - Mac OS (m1) 148 | - Другая 149 | validations: 150 | required: true 151 | - type: textarea 152 | id: logs 153 | attributes: 154 | label: Логи 155 | description: Скопируйте и вставьте сюда логи. Не забудьте скрыть чувствительные 156 | данные. 157 | render: Shell 158 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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-beta114 26 | - 0.2.0-beta113 27 | - 0.2.0-beta112 28 | - 0.2.0-beta111 29 | - 0.2.0-beta110 30 | - 0.2.0-beta109 31 | - 0.2.0-beta108 32 | - 0.2.0-beta107 33 | - 0.2.0-beta106 34 | - 0.2.0-beta105 35 | - 0.2.0-beta104 36 | - 0.2.0-beta103 37 | - 0.2.0-beta101 38 | - 0.2.0-beta100 39 | - 0.2.0-beta99 40 | - 0.2.0-beta98 41 | - 0.2.0-beta97 42 | - 0.2.0-beta96 43 | - 0.2.0-beta95 44 | - 0.2.0-beta94 45 | - 0.2.0-beta93 46 | - 0.2.0-beta92 47 | - 0.2.0-beta91 48 | - 0.2.0-beta90 49 | - 0.2.0-beta89 50 | - 0.2.0-beta88 51 | - 0.2.0-beta87 52 | - 0.2.0-beta86 53 | - 0.2.0-beta85 54 | - 0.2.0-beta84 55 | - 0.2.0-beta83 56 | - 0.2.0-beta82 57 | - 0.2.0-beta81 58 | - 0.2.0-beta80 59 | - 0.2.0-beta79 60 | - 0.2.0-beta78 61 | - 0.2.0-beta77 62 | - 0.2.0-beta76 63 | - 0.2.0-beta75 64 | - 0.2.0-beta74 65 | - 0.2.0-beta73 66 | - 0.2.0-beta72 67 | - 0.2.0-beta71 68 | - 0.2.0-beta70 69 | - 0.2.0-beta69 70 | - 0.2.0-beta68 71 | - 0.2.0-beta67 72 | - 0.2.0-beta66 73 | - 0.2.0-beta65 74 | - 0.2.0-beta64 75 | - 0.2.0-beta63 76 | - 0.2.0-beta62 77 | - 0.2.0-beta61 78 | - 0.2.0-beta60 79 | - 0.2.0-beta59 80 | - 0.2.0-beta58 81 | - 0.2.0-beta57 82 | - 0.2.0-beta56 83 | - 0.2.0-beta55 84 | - 0.2.0-beta54 85 | - 0.2.0-beta53 86 | - 0.2.0-beta52 87 | - 0.2.0-beta51 88 | - 0.2.0-beta50 89 | - 0.2.0-beta49 90 | - 0.2.0-beta48 91 | - 0.2.0-beta47 92 | - 0.2.0-beta46 93 | - 0.2.0-beta45 94 | - 0.2.0-beta44 95 | - 0.2.0-beta43 96 | - 0.2.0-beta42 97 | - 0.2.0-beta41 98 | - 0.2.0-beta40 99 | - 0.2.0-beta39 100 | - 0.2.0-beta38 101 | - 0.2.0-beta37 102 | - 0.2.0-beta36 103 | - 0.2.0-beta35 104 | - 0.2.0-beta34 105 | - 0.2.0-beta33 106 | - 0.2.0-beta32 107 | - 0.2.0-beta31 108 | - 0.2.0-beta30 109 | - 0.2.0-beta29 110 | - 0.2.0-beta28 111 | - 0.2.0-beta27 112 | - Другая 113 | validations: 114 | required: true 115 | - type: dropdown 116 | id: python-version 117 | attributes: 118 | label: Python Version 119 | description: Какая версия Python-а используется? 120 | options: 121 | - '3.11' 122 | - '3.10' 123 | - '3.9' 124 | - '3.8' 125 | - Другая 126 | validations: 127 | required: true 128 | - type: dropdown 129 | id: os 130 | attributes: 131 | label: OS 132 | description: Ваша операционная система. 133 | options: 134 | - Windows 135 | - Linux 136 | - Mac OS 137 | - Mac OS (m1) 138 | - Другая 139 | validations: 140 | required: true 141 | - type: textarea 142 | id: logs 143 | attributes: 144 | label: Логи 145 | description: Скопируйте и вставьте сюда логи. Не забудьте скрыть чувствительные 146 | данные. 147 | render: Shell 148 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | You can see [the commits](https://github.com/RussianInvestments/invest-python/commits/main) 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinkoff Invest 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/tinkoff-investments)](https://pypi.org/project/tinkoff-investments/) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tinkoff-investments)](https://www.python.org/downloads/) 5 | ![GitHub](https://img.shields.io/github/license/tinkoff/invest-python) 6 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/tinkoff-investments) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/api/clients.md: -------------------------------------------------------------------------------- 1 | 2 | # Clients 3 | 4 | ::: tinkoff.invest.clients 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/examples/__init__.py -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /examples/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 | -------------------------------------------------------------------------------- /examples/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 | -------------------------------------------------------------------------------- /examples/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/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/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 | -------------------------------------------------------------------------------- /examples/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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_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/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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/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/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/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 | -------------------------------------------------------------------------------- /examples/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/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/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 | -------------------------------------------------------------------------------- /examples/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /examples/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/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 | -------------------------------------------------------------------------------- /examples/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.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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /examples/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /protos/tinkoff/invest/grpc/google/api/field_behavior.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/protobuf/descriptor.proto"; 20 | 21 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "FieldBehaviorProto"; 24 | option java_package = "com.google.api"; 25 | option objc_class_prefix = "GAPI"; 26 | 27 | extend google.protobuf.FieldOptions { 28 | // A designation of a specific field behavior (required, output only, etc.) 29 | // in protobuf messages. 30 | // 31 | // Examples: 32 | // 33 | // string name = 1 [(google.api.field_behavior) = REQUIRED]; 34 | // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; 35 | // google.protobuf.Duration ttl = 1 36 | // [(google.api.field_behavior) = INPUT_ONLY]; 37 | // google.protobuf.Timestamp expire_time = 1 38 | // [(google.api.field_behavior) = OUTPUT_ONLY, 39 | // (google.api.field_behavior) = IMMUTABLE]; 40 | repeated google.api.FieldBehavior field_behavior = 1052; 41 | } 42 | 43 | // An indicator of the behavior of a given field (for example, that a field 44 | // is required in requests, or given as output but ignored as input). 45 | // This **does not** change the behavior in protocol buffers itself; it only 46 | // denotes the behavior and may affect how API tooling handles the field. 47 | // 48 | // Note: This enum **may** receive new values in the future. 49 | enum FieldBehavior { 50 | // Conventional default for enums. Do not use this. 51 | FIELD_BEHAVIOR_UNSPECIFIED = 0; 52 | 53 | // Specifically denotes a field as optional. 54 | // While all fields in protocol buffers are optional, this may be specified 55 | // for emphasis if appropriate. 56 | OPTIONAL = 1; 57 | 58 | // Denotes a field as required. 59 | // This indicates that the field **must** be provided as part of the request, 60 | // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). 61 | REQUIRED = 2; 62 | 63 | // Denotes a field as output only. 64 | // This indicates that the field is provided in responses, but including the 65 | // field in a request does nothing (the server *must* ignore it and 66 | // *must not* throw an error as a result of the field's presence). 67 | OUTPUT_ONLY = 3; 68 | 69 | // Denotes a field as input only. 70 | // This indicates that the field is provided in requests, and the 71 | // corresponding field is not included in output. 72 | INPUT_ONLY = 4; 73 | 74 | // Denotes a field as immutable. 75 | // This indicates that the field may be set once in a request to create a 76 | // resource, but may not be changed thereafter. 77 | IMMUTABLE = 5; 78 | 79 | // Denotes that a (repeated) field is an unordered list. 80 | // This indicates that the service may provide the elements of the list 81 | // in any arbitrary order, rather than the order the user originally 82 | // provided. Additionally, the list's order may or may not be stable. 83 | UNORDERED_LIST = 6; 84 | 85 | // Denotes that this field returns a non-empty default value if not set. 86 | // This indicates that if the user provides the empty value in a request, 87 | // a non-empty value will be returned. The user will not be aware of what 88 | // non-empty value to expect. 89 | NON_EMPTY_DEFAULT = 7; 90 | } 91 | -------------------------------------------------------------------------------- /protos/tinkoff/invest/grpc/sandbox.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tinkoff.public.invest.api.contract.v1; 4 | 5 | option go_package = "./;investapi"; 6 | option java_package = "ru.tinkoff.piapi.contract.v1"; 7 | option java_multiple_files = true; 8 | option csharp_namespace = "Tinkoff.InvestApi.V1"; 9 | option objc_class_prefix = "TIAPI"; 10 | option php_namespace = "Tinkoff\\Invest\\V1"; 11 | 12 | import "tinkoff/invest/grpc/common.proto"; 13 | import "tinkoff/invest/grpc/orders.proto"; 14 | import "tinkoff/invest/grpc/operations.proto"; 15 | import "tinkoff/invest/grpc/users.proto"; 16 | import "tinkoff/invest/grpc/google/api/field_behavior.proto"; 17 | 18 | service SandboxService { // Методы для работы с песочницей T-Invest API 19 | 20 | //OpenSandboxAccount — зарегистрировать счет 21 | rpc OpenSandboxAccount(OpenSandboxAccountRequest) returns (OpenSandboxAccountResponse); 22 | 23 | //GetSandboxAccounts — счета пользователя 24 | rpc GetSandboxAccounts(GetAccountsRequest) returns (GetAccountsResponse); 25 | 26 | //CloseSandboxAccount — закрыть счет 27 | rpc CloseSandboxAccount(CloseSandboxAccountRequest) returns (CloseSandboxAccountResponse); 28 | 29 | //PostSandboxOrder — выставить заявку 30 | rpc PostSandboxOrder(PostOrderRequest) returns (PostOrderResponse); 31 | 32 | //PostSandboxOrderAsync — выставить заявку асинхронным методом 33 | //Особенности работы приведены в [статье](/invest/services/orders/async). 34 | rpc PostSandboxOrderAsync(PostOrderAsyncRequest) returns (PostOrderAsyncResponse); 35 | 36 | //ReplaceSandboxOrder — изменить выставленную заявку 37 | rpc ReplaceSandboxOrder(ReplaceOrderRequest) returns (PostOrderResponse); 38 | 39 | //GetSandboxOrders — получить список активных заявок по счету 40 | rpc GetSandboxOrders(GetOrdersRequest) returns (GetOrdersResponse); 41 | 42 | //CancelSandboxOrder — отменить заявку 43 | rpc CancelSandboxOrder(CancelOrderRequest) returns (CancelOrderResponse); 44 | 45 | //GetSandboxOrderState — получить статус торгового поручения 46 | rpc GetSandboxOrderState(GetOrderStateRequest) returns (OrderState); 47 | 48 | //GetSandboxPositions — список позиций по счету 49 | rpc GetSandboxPositions(PositionsRequest) returns (PositionsResponse); 50 | 51 | //GetSandboxOperations — список операций по счету 52 | //При работе с методом учитывайте [особенности взаимодействия](/invest/services/operations/operations_problems). 53 | rpc GetSandboxOperations(OperationsRequest) returns (OperationsResponse); 54 | 55 | //GetSandboxOperationsByCursor — список операций по счету с пагинацией 56 | //При работе с методом учитывайте [особенности взаимодействия](/invest/services/operations/operations_problems). 57 | rpc GetSandboxOperationsByCursor(GetOperationsByCursorRequest) returns (GetOperationsByCursorResponse); 58 | 59 | //GetSandboxPortfolio — портфель по счету 60 | rpc GetSandboxPortfolio(PortfolioRequest) returns (PortfolioResponse); 61 | 62 | //SandboxPayIn — пополнить счет. 63 | rpc SandboxPayIn(SandboxPayInRequest) returns (SandboxPayInResponse); 64 | 65 | //GetSandboxWithdrawLimits — доступный остаток для вывода средств 66 | rpc GetSandboxWithdrawLimits(WithdrawLimitsRequest) returns (WithdrawLimitsResponse); 67 | 68 | //GetSandboxMaxLots — расчет количества доступных для покупки/продажи лотов 69 | rpc GetSandboxMaxLots(GetMaxLotsRequest) returns (GetMaxLotsResponse); 70 | } 71 | 72 | //Запрос открытия счета в песочнице. 73 | message OpenSandboxAccountRequest { 74 | optional string name = 1; // Название счета 75 | } 76 | 77 | //Номер открытого счета в песочнице. 78 | message OpenSandboxAccountResponse { 79 | string account_id = 1; //Номер счета 80 | } 81 | 82 | //Запрос закрытия счета в песочнице. 83 | message CloseSandboxAccountRequest { 84 | string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Номер счета 85 | } 86 | 87 | //Результат закрытия счета в песочнице. 88 | message CloseSandboxAccountResponse { 89 | //пустой ответ 90 | } 91 | 92 | //Запрос пополнения счета в песочнице. 93 | message SandboxPayInRequest { 94 | string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Номер счета 95 | MoneyValue amount = 2 [(google.api.field_behavior) = REQUIRED]; //Сумма пополнения счета в рублях 96 | } 97 | 98 | //Результат пополнения счета, текущий баланс. 99 | message SandboxPayInResponse { 100 | MoneyValue balance = 1; //Текущий баланс счета 101 | } 102 | -------------------------------------------------------------------------------- /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-beta114" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/scripts/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tests/__init__.py -------------------------------------------------------------------------------- /tests/caches/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tests/caches/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/data_loaders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tests/data_loaders/__init__.py -------------------------------------------------------------------------------- /tests/data_loaders/test_round_datetime_range.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Tuple 3 | 4 | import pytest 5 | 6 | from tinkoff.invest import CandleInterval 7 | from tinkoff.invest.utils import round_datetime_range 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("interval", "date_range", "expected_range"), 12 | [ 13 | ( 14 | CandleInterval.CANDLE_INTERVAL_1_MIN, 15 | ( 16 | datetime( 17 | year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1 18 | ), 19 | datetime( 20 | year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1 21 | ), 22 | ), 23 | ( 24 | datetime( 25 | year=2023, month=1, day=1, hour=1, minute=1, second=0, microsecond=0 26 | ), 27 | datetime( 28 | year=2023, month=1, day=2, hour=1, minute=2, second=0, microsecond=0 29 | ), 30 | ), 31 | ), 32 | ( 33 | CandleInterval.CANDLE_INTERVAL_HOUR, 34 | ( 35 | datetime( 36 | year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1 37 | ), 38 | datetime( 39 | year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1 40 | ), 41 | ), 42 | ( 43 | datetime( 44 | year=2023, month=1, day=1, hour=1, minute=0, second=0, microsecond=0 45 | ), 46 | datetime( 47 | year=2023, month=1, day=2, hour=2, minute=0, second=0, microsecond=0 48 | ), 49 | ), 50 | ), 51 | ( 52 | CandleInterval.CANDLE_INTERVAL_DAY, 53 | ( 54 | datetime( 55 | year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1 56 | ), 57 | datetime( 58 | year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1 59 | ), 60 | ), 61 | ( 62 | datetime( 63 | year=2023, month=1, day=1, hour=0, minute=0, second=0, microsecond=0 64 | ), 65 | datetime( 66 | year=2023, month=1, day=3, hour=0, minute=0, second=0, microsecond=0 67 | ), 68 | ), 69 | ), 70 | ( 71 | CandleInterval.CANDLE_INTERVAL_WEEK, 72 | ( 73 | datetime( 74 | year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1 75 | ), 76 | datetime( 77 | year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1 78 | ), 79 | ), 80 | ( 81 | datetime( 82 | year=2023, month=1, day=1, hour=0, minute=0, second=0, microsecond=0 83 | ), 84 | datetime( 85 | year=2023, month=1, day=9, hour=0, minute=0, second=0, microsecond=0 86 | ), 87 | ), 88 | ), 89 | ( 90 | CandleInterval.CANDLE_INTERVAL_MONTH, 91 | ( 92 | datetime( 93 | year=2023, month=1, day=1, hour=1, minute=1, second=1, microsecond=1 94 | ), 95 | datetime( 96 | year=2023, month=1, day=2, hour=1, minute=1, second=1, microsecond=1 97 | ), 98 | ), 99 | ( 100 | datetime( 101 | year=2023, month=1, day=1, hour=0, minute=0, second=0, microsecond=0 102 | ), 103 | datetime( 104 | year=2023, month=2, day=1, hour=0, minute=0, second=0, microsecond=0 105 | ), 106 | ), 107 | ), 108 | ], 109 | ) 110 | def test_round_datetime_range( 111 | interval: CandleInterval, 112 | date_range: Tuple[datetime, datetime], 113 | expected_range: Tuple[datetime, datetime], 114 | ): 115 | actual_range = round_datetime_range( 116 | date_range=date_range, 117 | interval=interval, 118 | ) 119 | 120 | assert actual_range == expected_range 121 | -------------------------------------------------------------------------------- /tests/marketdata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tests/marketdata/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/test_orders_canceling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tests/test_orders_canceling/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_strategies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tests/test_strategies/__init__.py -------------------------------------------------------------------------------- /tests/test_strategies/test_moving_average/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tests/test_strategies/test_moving_average/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | datetime(2022, 1, 25, 0, 0), 23 | datetime(2022, 1, 25, 0, 1), 24 | ), 25 | ], 26 | ), 27 | ( 28 | CandleInterval.CANDLE_INTERVAL_DAY, 29 | (datetime(2021, 1, 25, 0, 0), datetime(2022, 1, 25, 0, 0)), 30 | [ 31 | ( 32 | datetime(2021, 1, 25, 0, 0), 33 | datetime(2022, 1, 25, 0, 0), 34 | ), 35 | ], 36 | ), 37 | ( 38 | CandleInterval.CANDLE_INTERVAL_DAY, 39 | (datetime(2021, 1, 25, 0, 0), datetime(2022, 1, 24, 0, 0)), 40 | [ 41 | ( 42 | datetime(2021, 1, 25, 0, 0), 43 | datetime(2022, 1, 24, 0, 0), 44 | ), 45 | ], 46 | ), 47 | ], 48 | ) 49 | def test_get_intervals(candle_interval, interval, intervals): 50 | result = list( 51 | get_intervals( 52 | candle_interval, 53 | *interval, 54 | ) 55 | ) 56 | 57 | assert result == intervals 58 | 59 | 60 | @pytest.mark.parametrize( 61 | "s, expected", 62 | [ 63 | ("", True), 64 | ("123", False), 65 | ("1234567890", False), 66 | ("12345678-1234-1234-1234-abcdabcdabcd", True), 67 | ("12345678-12g4-1234-1234-abcdabcdabcd", False), 68 | ], 69 | ) 70 | def test_is_empty_or_uuid(s: str, expected: bool): 71 | assert expected == empty_or_uuid(s) 72 | -------------------------------------------------------------------------------- /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/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/__init__.py -------------------------------------------------------------------------------- /tinkoff/invest/caching/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/caching/__init__.py -------------------------------------------------------------------------------- /tinkoff/invest/caching/instruments_cache/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/caching/instruments_cache/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/caching/market_data_cache/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/caching/market_data_cache/__init__.py -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-beta114" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tinkoff/invest/grpc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/grpc/__init__.py -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tinkoff/invest/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import namedtuple 3 | from contextvars import ContextVar 4 | from typing import Any, Optional 5 | 6 | from .constants import ( 7 | MESSAGE, 8 | X_RATELIMIT_LIMIT, 9 | X_RATELIMIT_REMAINING, 10 | X_RATELIMIT_RESET, 11 | X_TRACKING_ID, 12 | ) 13 | 14 | __all__ = ( 15 | "get_current_tracking_id", 16 | "get_tracking_id_from_call", 17 | "get_tracking_id_from_coro", 18 | "get_metadata_from_call", 19 | "get_metadata_from_aio_error", 20 | "log_request", 21 | "log_error", 22 | ) 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | _TRACKING_ID: ContextVar[Optional[str]] = ContextVar("tracking_id", default=None) 27 | Metadata = namedtuple( 28 | "Metadata", 29 | ( 30 | "tracking_id", 31 | "ratelimit_limit", 32 | "ratelimit_remaining", 33 | "ratelimit_reset", 34 | "message", 35 | ), 36 | ) 37 | 38 | 39 | def get_current_tracking_id() -> Optional[str]: 40 | return _TRACKING_ID.get() or None 41 | 42 | 43 | def log_request(tracking_id: Optional[str], name: str) -> None: 44 | _TRACKING_ID.set(tracking_id) 45 | logger.info("%s %s", tracking_id, name) 46 | 47 | 48 | def log_error(tracking_id: Optional[str], name: str, text: str) -> None: 49 | _TRACKING_ID.set(tracking_id) 50 | logger.error("%s %s %s", tracking_id, name, text) 51 | 52 | 53 | def get_tracking_id_from_call(call: Any) -> Optional[str]: 54 | metadata = call.initial_metadata() or call.trailing_metadata() 55 | for item in metadata: 56 | if item.key == X_TRACKING_ID: 57 | return item.value 58 | return None 59 | 60 | 61 | async def get_tracking_id_from_coro(coro: Any) -> Optional[str]: 62 | metadata = await coro.initial_metadata() or await coro.trailing_metadata() 63 | for key, value in metadata: 64 | if key == X_TRACKING_ID: 65 | return value 66 | return None 67 | 68 | 69 | def get_metadata_from_call(call: Any) -> Optional[Metadata]: 70 | metadata = call.initial_metadata() or call.trailing_metadata() 71 | tracking_id = None 72 | ratelimit_limit = None 73 | ratelimit_remaining = None 74 | ratelimit_reset = None 75 | message = None 76 | for item in metadata: 77 | if item.key == X_TRACKING_ID: 78 | tracking_id = item.value 79 | elif item.key == X_RATELIMIT_LIMIT: 80 | ratelimit_limit = item.value 81 | elif item.key == X_RATELIMIT_REMAINING: 82 | ratelimit_remaining = int(item.value) 83 | elif item.key == X_RATELIMIT_RESET: 84 | ratelimit_reset = int(item.value) 85 | elif item.key == MESSAGE: 86 | message = item.value 87 | if not any( 88 | (tracking_id, ratelimit_limit, ratelimit_remaining, ratelimit_reset, message) 89 | ): 90 | return None 91 | return Metadata( 92 | tracking_id, ratelimit_limit, ratelimit_remaining, ratelimit_reset, message 93 | ) 94 | 95 | 96 | def get_metadata_from_aio_error(err: Any) -> Optional[Metadata]: 97 | metadata = err.initial_metadata() or err.trailing_metadata() 98 | tracking_id = None 99 | ratelimit_limit = None 100 | ratelimit_remaining = None 101 | ratelimit_reset = None 102 | message = None 103 | for key, value in metadata: 104 | if key == X_TRACKING_ID: 105 | tracking_id = value 106 | elif key == X_RATELIMIT_LIMIT: 107 | ratelimit_limit = value 108 | elif key == X_RATELIMIT_REMAINING: 109 | ratelimit_remaining = int(value) 110 | elif key == X_RATELIMIT_RESET: 111 | ratelimit_reset = int(value) 112 | elif key == MESSAGE: 113 | message = value 114 | if not any( 115 | (tracking_id, ratelimit_limit, ratelimit_remaining, ratelimit_reset, message) 116 | ): 117 | return None 118 | return Metadata( 119 | tracking_id, ratelimit_limit, ratelimit_remaining, ratelimit_reset, message 120 | ) 121 | -------------------------------------------------------------------------------- /tinkoff/invest/market_data_stream/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/market_data_stream/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /tinkoff/invest/market_data_stream/typevars.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | TMarketDataStreamManager = TypeVar("TMarketDataStreamManager") 4 | TInstrument = TypeVar("TInstrument") 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tinkoff/invest/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/py.typed -------------------------------------------------------------------------------- /tinkoff/invest/retrying/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/retrying/__init__.py -------------------------------------------------------------------------------- /tinkoff/invest/retrying/aio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/retrying/aio/__init__.py -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/retrying/sync/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/retrying/sync/__init__.py -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tinkoff/invest/sandbox/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/sandbox/__init__.py -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /tinkoff/invest/strategies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/strategies/__init__.py -------------------------------------------------------------------------------- /tinkoff/invest/strategies/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/strategies/base/__init__.py -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/strategies/moving_average/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/strategies/moving_average/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tinkoff/invest/strategies/plotting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RussianInvestments/invest-python/d7870e8c53af8db3ff7026fcc486c38fad6ec3e6/tinkoff/invest/strategies/plotting/__init__.py -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------