├── .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 | [](https://pypi.org/project/tinkoff-investments/)
4 | [](https://www.python.org/downloads/)
5 | 
6 | 
7 |
8 | Данный репозиторий предоставляет клиент для взаимодействия с торговой платформой [Тинькофф Инвестиции](https://www.tinkoff.ru/invest/) на языке Python.
9 |
10 | - [Документация](https://RussianInvestments.github.io/invest-python/)
11 | - [Документация по Invest API](https://RussianInvestments.github.io/investAPI/)
12 |
13 | ## Начало работы
14 |
15 |
16 |
17 | ```
18 | $ pip install tinkoff-investments
19 | ```
20 |
21 | ## Возможности
22 |
23 | - ☑ Синхронный и асинхронный GRPC клиент
24 | - ☑ Возможность отменить все заявки
25 | - ☑ Выгрузка истории котировок "от" и "до"
26 | - ☑ Кеширование данных
27 | - ☑ Торговая стратегия
28 |
29 | ## Как пользоваться
30 |
31 | ### Получить список аккаунтов
32 |
33 | ```python
34 | from tinkoff.invest import Client
35 |
36 | TOKEN = 'token'
37 |
38 | with Client(TOKEN) as client:
39 | print(client.users.get_accounts())
40 | ```
41 |
42 | ### Переопределить target
43 |
44 | В Tinkoff Invest API есть два контура - "боевой", предназначенный для исполнения ордеров на бирже и "песочница", предназначенный для тестирования API и торговых гипотез, заявки с которого не выводятся на биржу, а исполняются в эмуляторе.
45 |
46 | Переключение между контурами реализовано через target, INVEST_GRPC_API - "боевой", INVEST_GRPC_API_SANDBOX - "песочница"
47 |
48 | ```python
49 | from tinkoff.invest import Client
50 | from tinkoff.invest.constants import INVEST_GRPC_API
51 |
52 | TOKEN = 'token'
53 |
54 | with Client(TOKEN, target=INVEST_GRPC_API) as client:
55 | print(client.users.get_accounts())
56 | ```
57 |
58 | > :warning: **Не публикуйте токены в общедоступные репозитории**
59 |
60 |
61 | Остальные примеры доступны в [examples](https://github.com/RussianInvestments/invest-python/tree/main/examples).
62 |
63 | ## Contribution
64 |
65 | Для тех, кто хочет внести свои изменения в проект.
66 |
67 | - [CONTRIBUTING](https://github.com/RussianInvestments/invest-python/blob/main/CONTRIBUTING.md)
68 |
69 | ## License
70 |
71 | Лицензия [The Apache License](https://github.com/RussianInvestments/invest-python/blob/main/LICENSE).
72 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------