feat(reports): протокол и парсеры отчётов Сбера, ВТБ и Snowball CSV

sources/reports/base.py — контракт ReportParser (плагины: sniff/parse, чистые
функции без БД). registry.py выбирает парсер по содержимому файла, CSV
последним: он узнаёт файл по набору колонок и иначе перехватил бы чужой
формат.

Комиссия капитализируется в сделку, отдельным событием не эмитится: и Сбер,
и ВТБ печатают её дважды — колонками в сделках и строками в движении денег,
суммы совпадают, второе прочтение задвоило бы её. Расчётные строки («Сделка
от …», «Сальдо расчетов по сделкам») не эмитятся — это денежные ноги уже
учтённых сделок. У Сбера таблица «Информация о зачислениях на ИИС»
кумулятивна за календарный год и в леджер не идёт, иначе два пересекающихся
отчёта задвоили бы пополнения. CSV Snowball сводный по всем брокерам —
годится как сверка потоков и сделок на уровне портфеля, но не позиций по
счетам; CUSTOM_HOLDING_PRICE не событие, а цена — уходит в meta для
price_manual.

Обезличивание — anonymize.py + scripts/anonymize_reports.py, секреты
собираются по всему корпусу отчётов разом (Snowball цитирует номер договора
Сбера в примечании к переводу). test_fixtures_anonymized.py падает, если
в tests/fixtures/reports/ вне raw/ найдётся ИНН, ФИО или номер счёта — и по
форме (работает в CI без raw/), и по фактическому содержимому raw/, когда оно
на месте.
This commit is contained in:
Dmitry
2026-09-19 10:39:37 +03:00
parent 6b32c79e79
commit 2e742a093b
25 changed files with 7284 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,546 @@
Event,Date,Symbol,Price,Quantity,Currency,FeeTax,Exchange,NKD,FeeCurrency,DoNotAdjustCash,Note
"CASH_IN","2025-02-26 09:22:54","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"CASH_OUT","2025-02-26 09:22:54","RUB","1","0,002832","RUB","0","","","","False","Эта сделка добавлена с целью корректировки баланса по этой валюте, т.к. баланс по данным брокера (459,38) не соответствует балансу рассчитанному по сделкам (459,38)."
"BUY","2025-02-26 09:23:29","DELI","219","10","RUB","6,57","MCX","","","False",""
"BUY","2025-02-26 09:24:09","ROSN","581,25","2","RUB","3,49","MCX","","","False",""
"BUY","2025-02-26 09:25:23","DELI","219","7","RUB","4,6","MCX","","","False",""
"CASH_IN","2025-02-27 19:28:34","RUB","1","3400","RUB","0","","","","False","Пополнение счета"
"BUY","2025-02-27 19:28:47","SBERP","307,39","10","RUB","9,22","MCX","","","False",""
"BUY","2025-02-28 08:49:02","DELI","211,45","1","RUB","0,63","MCX","","","False",""
"CASH_IN","2025-03-12 16:12:53","RUB","1","10000","RUB","0","","","","False","Пополнение счета"
"BUY","2025-03-12 16:13:39","DELI","196,3","12","RUB","7,07","MCX","","","False",""
"BUY","2025-03-12 16:13:49","DELI","196,3","10","RUB","5,89","MCX","","","False",""
"BUY","2025-03-12 16:14:20","DELI","196,4","10","RUB","5,89","MCX","","","False",""
"BUY","2025-03-12 16:18:28","RU000A107AM4","929,25","4","RUB","11,15","MCX","5,08","","False",""
"CASH_IN","2025-03-12 16:19:39","RUB","1","197,7","RUB","0","","","","False","Пополнение счета"
"BUY","2025-03-12 16:19:40","DELI","196,5","1","RUB","0,59","MCX","","","False",""
"BUY","2025-03-24 16:28:49","ASTR","427,45","2","RUB","2,56","MCX","","","False",""
"CASH_IN","2025-03-24 16:28:49","RUB","1","860,08","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-03-25 18:00:22","RUB","1","5007,5","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-03-25 18:02:09","RUB","1","5508,25","RUB","0","","","","False","Пополнение счета"
"BUY","2025-03-25 18:17:42","WUSH","217,552","5","RUB","3,26","MCX","","","False",""
"BUY","2025-03-25 18:21:55","TATNP","647,5","2","RUB","3,89","MCX","","","False",""
"BUY","2025-03-25 18:24:32","SFIN","1538,8","1","RUB","4,62","MCX","","","False",""
"BUY","2025-03-25 18:24:41","LENT","1384","1","RUB","4,15","MCX","","","False",""
"BUY","2025-03-25 18:25:23","IVAT","187,5","1","RUB","0,56","MCX","","","False",""
"BUY","2025-03-26 17:27:00","TPAY","96,5","1","RUB","0","MCX","","","False",""
"BUY","2025-04-02 11:22:50","RU000A10B7T7","1000","5","RUB","7,5","MCX","0","","False",""
"DIVIDEND","2025-04-09 18:40:43","TPAY","0","1,94","RUB","0","MCX","","","False",""
"CASH_IN","2025-04-11 18:24:33","RUB","1","8745,17","RUB","0","","","","False","Пополнение счета"
"CASH_OUT","2025-04-11 18:24:33","RUB","1","0,010799","RUB","0","","","","False","Эта сделка добавлена с целью корректировки баланса по этой валюте, т.к. баланс по данным брокера (364,86) не соответствует балансу рассчитанному по сделкам (364,87)."
"BUY","2025-04-11 18:25:10","WUSH","181,58","10","RUB","5,45","MCX","","","False",""
"BUY","2025-04-11 18:25:27","IVAT","152,99","10","RUB","4,59","MCX","","","False",""
"BUY","2025-04-11 18:25:58","ROSN","450,566667","3","RUB","4,06","MCX","","","False",""
"BUY","2025-04-11 18:26:54","PIKK","427,3","3","RUB","3,85","MCX","","","False",""
"BUY","2025-04-11 18:28:10","CNRU","539,8","3","RUB","4,86","MCX","","","False",""
"BUY","2025-04-18 09:52:48","ROSN","475,05","2","RUB","2,85","MCX","","","False",""
"CASH_IN","2025-04-25 16:11:11","RUB","1","1626,6","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-04-25 16:11:25","RUB","1","1626,6","RUB","0","","","","False","Пополнение счета"
"BUY","2025-04-28 10:34:11","TPAY","96,76","17","RUB","0","MCX","","","False",""
"BUY","2025-04-28 10:35:25","NVTK","1259,3","1","RUB","3,78","MCX","","","False",""
"CASH_IN","2025-04-29 19:39:31","RUB","1","2610","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-04-29 19:39:41","RUB","1","2610","RUB","0","","","","False","Пополнение счета"
"BUY","2025-04-30 10:50:36","TPAY","95,47","2","RUB","0","MCX","","","False",""
"BUY","2025-04-30 10:50:48","RU000A107AM4","935,8","1","RUB","2,81","MCX","23,23","","False",""
"BUY","2025-04-30 10:51:05","DELI","169,4","8","RUB","4,07","MCX","","","False",""
"BUY","2025-04-30 10:51:20","CNRU","549","2","RUB","3,29","MCX","","","False",""
"BUY","2025-04-30 10:51:32","PIKK","470,8","2","RUB","2,82","MCX","","","False",""
"BUY","2025-04-30 10:52:36","APTK","10,412","100","RUB","3,12","MCX","","","False",""
"CASH_IN","2025-05-12 19:51:08","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-05-12 19:51:19","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2025-05-14 10:43:57","EUTR","125,25","10","RUB","3,76","MCX","","","False",""
"BUY","2025-05-14 10:45:03","DELI","163,6","1","RUB","0,49","MCX","","","False",""
"BUY","2025-05-14 10:45:16","ROSN","451,2","8","RUB","10,83","MCX","","","False",""
"BUY","2025-05-14 10:45:36","PIKK","490,58","5","RUB","7,36","MCX","","","False",""
"BUY","2025-05-14 10:46:01","NVTK","1192,55","2","RUB","7,16","MCX","","","False",""
"DIVIDEND","2025-05-15 17:05:35","TPAY","0","25,72","RUB","0","MCX","","","False",""
"CASH_IN","2025-05-23 16:12:44","RUB","1","1200","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-05-23 16:12:54","RUB","1","1200","RUB","0","","","","False","Пополнение счета"
"BUY","2025-05-27 08:55:30","DELI","157,65","8","RUB","3,78","MCX","","","False",""
"BUY","2025-05-27 08:55:47","DELI","157,65","8","RUB","3,78","MCX","","","False",""
"DIVIDEND","2025-05-30 09:12:44","RU000A107AM4","0","165,15","RUB","0","MCX","","","False",""
"CASH_IN","2025-06-05 12:46:14","RUB","1","2600","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-06-05 12:46:24","RUB","1","2600","RUB","0","","","","False","Пополнение счета"
"BUY","2025-06-05 16:21:32","RU000A10B7T7","1038","2","RUB","6,23","MCX","44,52","","False",""
"BUY","2025-06-05 16:21:43","DELI","169,85","2","RUB","1,02","MCX","","","False",""
"BUY","2025-06-05 16:21:53","NVTK","1110,55","2","RUB","6,66","MCX","","","False",""
"BUY","2025-06-05 16:22:12","IVAT","128,35","5","RUB","1,93","MCX","","","False",""
"DIVIDEND","2025-06-09 15:56:44","TPAY","0","26,82","RUB","0","MCX","","","False",""
"DIVIDEND","2025-06-11 12:29:50","SFIN","0","83,5","RUB","9","MCX","","","False",""
"CASH_IN","2025-06-12 10:19:15","RUB","1","6000","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-06-12 10:19:29","RUB","1","6000","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2025-06-19 15:21:06","TATNP","0","86,22","RUB","11","MCX","","","False",""
"BUY","2025-06-19 16:22:31","TMOS","6,21","398","RUB","0","MCX","","","False",""
"BUY","2025-06-19 16:23:05","TMOS","6,21","398","RUB","0","MCX","","","False",""
"BUY","2025-06-19 16:24:05","SBRB","15,845","77","RUB","3,66","MCX","","","False",""
"BUY","2025-06-19 16:24:16","SBRB","15,845","77","RUB","3,66","MCX","","","False",""
"BUY","2025-06-19 16:25:10","TGLD","10,52","58","RUB","0","MCX","","","False",""
"BUY","2025-06-19 16:25:27","TGLD","10,52","58","RUB","0","MCX","","","False",""
"BUY","2025-06-19 16:26:09","LQDT","1,7196","356","RUB","1,84","MCX","","","False",""
"BUY","2025-06-19 16:26:26","LQDT","1,7196","356","RUB","1,84","MCX","","","False",""
"BUY","2025-06-19 16:27:03","TPAY","98,23","12","RUB","0","MCX","","","False",""
"BUY","2025-06-19 16:27:09","TPAY","98,23","12","RUB","0","MCX","","","False",""
"CASH_IN","2025-06-25 19:07:20","RUB","1","1200","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-06-25 19:07:31","RUB","1","1200","RUB","0","","","","False","Пополнение счета"
"AMORTISATION","2025-06-26 14:00:41","RU000A10B7T7","0","897,82","RUB","0","MCX","","","False",""
"DIVIDEND","2025-06-26 14:10:53","RU000A10B7T7","0","407,54","RUB","0","MCX","","","False",""
"BUY","2025-06-26 14:22:14","RU000A10B7T7","909,835","2","RUB","5,46","MCX","0,6","","False",""
"BUY","2025-06-26 14:22:34","RU000A10B7T7","909,92","1","RUB","2,73","MCX","0,6","","False",""
"CASH_IN","2025-07-04 15:45:31","RUB","1","2900","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-07-04 15:45:52","RUB","1","2900","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-07-04 23:49:07","RUB","1","2610","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-07-04 23:49:17","RUB","1","2610","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2025-07-08 12:33:19","TPAY","0","56,4","RUB","0","MCX","","","False",""
"DIVIDEND","2025-07-08 13:58:58","TPAY","0","20,15","RUB","0","MCX","","","False",""
"BUY","2025-07-08 14:24:10","TMOS","6,2","403","RUB","0","MCX","","","False",""
"BUY","2025-07-08 14:24:21","TMOS","6,2","403","RUB","0","MCX","","","False",""
"BUY","2025-07-08 14:24:50","TBRU","6,96","286","RUB","0","MCX","","","False",""
"BUY","2025-07-08 14:24:58","TBRU","6,96","286","RUB","0","MCX","","","False",""
"BUY","2025-07-08 14:25:57","TGLD","10,37","96","RUB","0","MCX","","","False",""
"BUY","2025-07-08 14:26:05","TGLD","10,37","96","RUB","0","MCX","","","False",""
"BUY","2025-07-08 14:26:34","TPAY","98,56","8","RUB","0","MCX","","","False",""
"BUY","2025-07-08 14:26:40","TPAY","98,56","4","RUB","0","MCX","","","False",""
"CASH_IN","2025-07-11 21:28:53","RUB","1","5300","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-07-11 21:29:03","RUB","1","5300","RUB","0","","","","False","Пополнение счета"
"BUY","2025-07-11 21:31:35","VTBR","72,84","13","RUB","2,84","MCX","","","False",""
"BUY","2025-07-11 21:31:44","VTBR","72,84","13","RUB","2,84","MCX","","","False",""
"BUY","2025-07-11 21:32:50","T","3129,4","1","RUB","0","MCX","","","False",""
"BUY","2025-07-11 21:32:58","T","3129,3","1","RUB","0","MCX","","","False",""
"DIVIDEND","2025-07-15 14:09:51","ASTR","0","6,29","RUB","1","MCX","","","False",""
"CASH_IN","2025-07-15 17:22:31","RUB","1","1000","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2025-07-15 17:23:34","RUB","1","1000","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2025-07-21 07:36:14","T","0","33","RUB","4","MCX","","","False",""
"DIVIDEND","2025-07-21 08:13:36","T","0","33","RUB","4","MCX","","","False",""
"DIVIDEND","2025-07-28 20:55:02","RU000A10B7T7","0","161,19","RUB","0","MCX","","","False",""
"DIVIDEND","2025-07-28 20:59:19","RU000A10B7T7","0","17,91","RUB","0","MCX","","","False",""
"AMORTISATION","2025-07-28 21:02:33","RU000A10B7T7","0","592,11","RUB","0","MCX","","","False",""
"AMORTISATION","2025-07-28 21:06:43","RU000A10B7T7","0","65,79","RUB","0","MCX","","","False",""
"DIVIDEND","2025-08-01 11:52:28","EUTR","0","141,9","RUB","19","MCX","","","False",""
"DIVIDEND","2025-08-01 13:35:14","EUTR","0","30","RUB","4","MCX","","","False",""
"DIVIDEND","2025-08-04 14:42:34","SBERP","0","348,4","RUB","45","MCX","","","False",""
"DIVIDEND","2025-08-04 19:55:40","ROSN","0","73,4","RUB","10","MCX","","","False",""
"DIVIDEND","2025-08-04 20:07:25","ROSN","0","146,8","RUB","19","MCX","","","False",""
"DIVIDEND","2025-08-08 15:01:23","TPAY","0","57,07","RUB","0","MCX","","","False",""
"DIVIDEND","2025-08-08 15:45:28","TPAY","0","23,43","RUB","0","MCX","","","False",""
"SELL","2025-08-08 17:15:27","PIKK","638,7","10","RUB","19,16","MCX","","","False",""
"SELL","2025-08-08 17:16:06","IVAT","168,3","6","RUB","3,03","MCX","","","False",""
"SELL","2025-08-08 17:16:16","IVAT","168,25","2","RUB","1,01","MCX","","","False",""
"CASH_OUT","2025-08-08 17:18:02","RUB","1","9900","RUB","0","","","","False","Вывод со счета"
"TAX","2025-08-12 06:03:36","","0","0","RUB","234","","","","False","Корректировка налога"
"SELL","2025-08-20 21:40:50","VTBR","77,91","13","RUB","3,04","MCX","","","False",""
"SELL","2025-08-20 21:41:08","EUTR","126,4","10","RUB","3,79","MCX","","","False",""
"SELL","2025-08-20 21:41:19","LENT","1785,5","1","RUB","5,36","MCX","","","False",""
"SELL","2025-08-20 21:41:56","IVAT","169,4","6","RUB","3,05","MCX","","","False",""
"SELL","2025-08-20 21:43:13","LQDT","1,7755","356","RUB","1,9","MCX","","","False",""
"SELL","2025-08-20 21:51:17","WUSH","140,58","5","RUB","2,11","MCX","","","False",""
"BUY","2025-08-21 10:50:16","TGLD","10,69","93","RUB","0","MCX","","","False",""
"BUY","2025-08-21 10:50:16","TMOS","6,76","369","RUB","0","MCX","","","False",""
"SELL","2025-08-21 11:17:38","TPAY","101,62","40","RUB","0","MCX","","","False",""
"BUY","2025-08-21 11:22:38","TMOS","6,75","381","RUB","0","MCX","","","False",""
"BUY","2025-08-21 11:22:39","TGLD","10,69","96","RUB","0","MCX","","","False",""
"BUY","2025-08-21 12:15:01","TMOS","6,75","251","RUB","0","MCX","","","False",""
"BUY","2025-08-21 12:15:01","TPAY","101,61","16","RUB","0","MCX","","","False",""
"BUY","2025-08-21 12:15:02","TGLD","10,69","63","RUB","0","MCX","","","False",""
"BUY","2025-08-21 12:26:45","SU26212RMFS9","887,83","1","RUB","2,66","MCX","5,79","","False",""
"BUY","2025-08-21 12:26:45","TPAY","101,6","7","RUB","0","MCX","","","False",""
"BUY","2025-08-21 12:26:46","TGLD","10,69","27","RUB","0","MCX","","","False",""
"BUY","2025-08-21 12:26:46","TMOS","6,76","107","RUB","0","MCX","","","False",""
"SELL","2025-08-21 12:43:14","ASTR","416,75","2","RUB","2,5","MCX","","","False",""
"SELL","2025-08-21 12:46:04","DELI","148,3","68","RUB","30,25","MCX","","","False",""
"SELL","2025-08-21 12:46:47","DELI","148,255","10","RUB","4,44","MCX","","","False",""
"BUY","2025-08-21 12:55:10","SU26212RMFS9","887,35","4","RUB","10,65","MCX","5,79","","False",""
"BUY","2025-08-21 12:55:11","TMOS","6,77","394","RUB","0","MCX","","","False",""
"BUY","2025-08-21 12:55:11","TPAY","101,58","26","RUB","0","MCX","","","False",""
"BUY","2025-08-21 12:55:12","TGLD","10,71","99","RUB","0","MCX","","","False",""
"BUY","2025-08-22 13:40:55","RNFT","124,5","50","RUB","18,68","MCX","","","False",""
"SELL","2025-08-22 13:45:06","TATN","675,1","10","RUB","20,25","MCX","","","False",""
"FEE","2025-08-23 00:30:53","","0","0","RUB","45","","","","False",""
"FEE","2025-08-24 00:32:22","","0","0","RUB","45","","","","False",""
"FEE","2025-08-25 00:31:52","","0","0","RUB","45","","","","False",""
"FEE","2025-08-26 00:32:25","","0","0","RUB","45","","","","False",""
"AMORTISATION","2025-08-26 13:06:06","RU000A10B7T7","0","664,47","RUB","0","MCX","","","False",""
"AMORTISATION","2025-08-26 13:07:38","RU000A10B7T7","0","73,83","RUB","0","MCX","","","False",""
"DIVIDEND","2025-08-26 13:14:31","RU000A10B7T7","0","153,99","RUB","0","MCX","","","False",""
"DIVIDEND","2025-08-26 13:14:58","RU000A10B7T7","0","17,11","RUB","0","MCX","","","False",""
"FEE","2025-08-27 00:36:17","","0","0","RUB","45","","","","False",""
"FEE","2025-08-28 00:34:11","","0","0","RUB","45","","","","False",""
"FEE","2025-08-29 00:33:46","","0","0","RUB","45","","","","False",""
"DIVIDEND","2025-08-29 13:54:42","RU000A107AM4","0","165,15","RUB","0","MCX","","","False",""
"BUY","2025-08-29 17:24:07","TATN","650,5","10","RUB","19,52","MCX","","","False",""
"SELL","2025-08-29 17:24:25","RNFT","121,2","50","RUB","18,18","MCX","","","False",""
"SELL","2025-09-01 17:59:16","VTBR","74,54","13","RUB","2,91","MCX","","","False",""
"SELL","2025-09-01 17:59:29","NVTK","1239,6","5","RUB","18,59","MCX","","","False",""
"SELL","2025-09-01 17:59:39","ROSN","461,32","5","RUB","6,92","MCX","","","False",""
"SELL","2025-09-01 18:02:17","IVAT","159,7","2","RUB","0,96","MCX","","","False",""
"SELL","2025-09-01 18:02:30","CNRU","607,4","5","RUB","9,11","MCX","","","False",""
"SELL","2025-09-01 18:02:39","T","3335,8","1","RUB","0","MCX","","","False",""
"SELL","2025-09-01 18:04:27","APTK","9,532","100","RUB","2,86","MCX","","","False",""
"SELL","2025-09-01 18:04:35","WUSH","125,45","10","RUB","3,77","MCX","","","False",""
"SELL","2025-09-01 18:04:53","RU000A10B7T7","769,384908","1","RUB","2,31","MCX","3,505092","","False",""
"TAX","2025-09-01 18:05:10","","0","0","RUB","3","","","","False","Удержание налога"
"CASH_OUT","2025-09-01 18:05:10","RUB","1","20358,92","RUB","0","","","","False","Вывод со счета"
"TAX_RETURN","2025-09-03 07:21:59","","0","0","RUB","3","","","","False","Корректировка налога"
"CASH_IN","2025-09-05 11:23:58","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2025-09-05 17:42:16","TPAY","0","60,89","RUB","0","MCX","","","False",""
"DIVIDEND","2025-09-05 18:09:43","TPAY","0","19,82","RUB","0","MCX","","","False",""
"BUY","2025-09-08 21:21:46","SU26212RMFS9","884,4","2","RUB","5,31","MCX","9,27","","False",""
"BUY","2025-09-08 21:21:47","TMOS","6,76","179","RUB","0","MCX","","","False",""
"BUY","2025-09-08 21:21:47","TPAY","100,84","12","RUB","0","MCX","","","False",""
"BUY","2025-09-08 21:22:16","SU26212RMFS9","884,4","1","RUB","2,65","MCX","9,27","","False",""
"BUY","2025-09-08 21:22:17","TMOS","6,76","107","RUB","0","MCX","","","False",""
"BUY","2025-09-08 21:22:17","TPAY","100,84","7","RUB","0","MCX","","","False",""
"AMORTISATION","2025-09-26 16:32:06","RU000A10B7T7","0","631,89","RUB","0","MCX","","","False",""
"DIVIDEND","2025-09-26 16:33:22","RU000A10B7T7","0","139,95","RUB","0","MCX","","","False",""
"CASH_IN","2025-10-05 13:15:58","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2025-10-06 20:20:29","SU26212RMFS9","869,77","2","RUB","5,22","MCX","14,68","","False",""
"BUY","2025-10-06 20:20:29","TPAY","100,02","12","RUB","0","MCX","","","False",""
"BUY","2025-10-06 20:20:30","TMOS","6,17","196","RUB","0","MCX","","","False",""
"SELL","2025-10-07 13:23:38","LQDT","1,8159","356","RUB","1,94","MCX","","","False",""
"SELL","2025-10-07 13:25:10","TGLD","12,88","154","RUB","0","MCX","","","False",""
"SELL","2025-10-07 13:25:31","TMOS","6,21","801","RUB","0","MCX","","","False",""
"SELL","2025-10-07 13:25:58","TPAY","100,1","16","RUB","0","MCX","","","False",""
"SELL","2025-10-07 13:26:18","SBRB","16,599","77","RUB","3,83","MCX","","","False",""
"SELL","2025-10-07 13:26:39","TBRU","7,39","286","RUB","0","MCX","","","False",""
"CASH_OUT","2025-10-07 13:27:12","RUB","1","12614,5","RUB","0","","","","False","Вывод со счета"
"DIVIDEND","2025-10-08 10:27:11","T","0","35","RUB","4","MCX","","","False",""
"DIVIDEND","2025-10-08 16:58:14","TPAY","0","93,27","RUB","0","MCX","","","False",""
"DIVIDEND","2025-10-08 17:28:20","TPAY","0","22,24","RUB","0","MCX","","","False",""
"TAX","2025-10-09 04:18:26","","0","0","RUB","22","","","","False","Корректировка налога"
"AMORTISATION","2025-10-27 18:30:50","RU000A10B7T7","0","570,42","RUB","0","MCX","","","False",""
"DIVIDEND","2025-10-27 19:16:05","RU000A10B7T7","0","122,4","RUB","0","MCX","","","False",""
"DIVIDEND","2025-10-29 14:52:01","TATNP","0","28,7","RUB","4","MCX","","","False",""
"CASH_IN","2025-11-05 11:58:33","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2025-11-06 12:12:16","SU26212RMFS9","879,766667","3","RUB","7,92","MCX","20,66999966666667","","False",""
"BUY","2025-11-06 12:12:17","TMOS","5,94","327","RUB","0","MCX","","","False",""
"BUY","2025-11-06 12:12:17","TPAY","100,49","19","RUB","0","MCX","","","False",""
"BUY","2025-11-06 12:12:18","TGLD","13","59","RUB","0","MCX","","","False",""
"DIVIDEND","2025-11-10 19:06:16","TPAY","0","95,8","RUB","0","MCX","","","False",""
"AMORTISATION","2025-11-26 18:13:31","RU000A10B7T7","0","558,09","RUB","0","MCX","","","False",""
"DIVIDEND","2025-11-26 18:21:54","RU000A10B7T7","0","114,39","RUB","0","MCX","","","False",""
"CASH_IN","2025-11-27 11:05:13","RUB","1","5818,41","RUB","0","","","","False","Пополнение счета"
"BUY","2025-11-27 11:05:14","ASTR","289,1","20","RUB","17,35","MCX","","","False",""
"CASH_IN","2025-11-27 11:05:59","RUB","1","3431,27","RUB","0","","","","False","Пополнение счета"
"BUY","2025-11-27 11:06:00","IVAT","170,25","20","RUB","10,22","MCX","","","False",""
"DIVIDEND","2025-11-28 10:29:07","RU000A107AM4","0","165,15","RUB","0","MCX","","","False",""
"CASH_IN","2025-12-05 14:15:14","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2025-12-08 19:27:48","TPAY","0","92,14","RUB","0","MCX","","","False",""
"DIVIDEND","2025-12-26 17:14:16","RU000A10B7T7","0","99,18","RUB","0","MCX","","","False",""
"AMORTISATION","2025-12-26 17:17:03","RU000A10B7T7","0","475,65","RUB","0","MCX","","","False",""
"DIVIDEND","2025-12-29 12:07:40","SFIN","0","902","RUB","117","MCX","","","False",""
"CASH_IN","2026-01-05 13:59:37","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-01-06 10:22:03","TPAY","100,27","34","RUB","0","MCX","","","False",""
"BUY","2026-01-06 10:22:04","TMOS","6,45","346","RUB","0","MCX","","","False",""
"SELL","2026-01-08 11:41:58","RU000A10B7T7","502,484763","9","RUB","13,57","MCX","4,639681444444444","","False",""
"SELL","2026-01-08 11:41:59","RU000A107AM4","991,7","5","RUB","14,88","MCX","15,61","","False",""
"SELL","2026-01-08 11:41:59","SFIN","829,2","1","RUB","2,49","MCX","","","False",""
"SELL","2026-01-08 11:41:59","T","3255","1","RUB","0","MCX","","","False",""
"SELL","2026-01-08 11:42:00","SBERP","297,89","10","RUB","8,94","MCX","","","False",""
"SELL","2026-01-08 11:42:00","SU26212RMFS9","890,520769","13","RUB","34,73","MCX","32,83000023076923","","False",""
"SELL","2026-01-08 11:42:00","TATNP","535,4","2","RUB","3,21","MCX","","","False",""
"SELL","2026-01-08 11:42:01","ROSN","400,45","10","RUB","12,01","MCX","","","False",""
"BUY","2026-01-08 11:42:01","TMOS","6,37","2114","RUB","0","MCX","","","False",""
"BUY","2026-01-08 11:42:02","SBRB","17,683","173","RUB","9,18","MCX","","","False",""
"BUY","2026-01-08 11:42:02","TBRU","7,81","1986","RUB","0","MCX","","","False",""
"BUY","2026-01-08 11:42:02","TPAY","100,39","43","RUB","0","MCX","","","False",""
"BUY","2026-01-12 18:40:57","TDIV","10,269799","696","RUB","0","MCX","","","False",""
"SELL","2026-01-12 18:40:57","TMOS","6,380144","695","RUB","0","MCX","","","False",""
"SELL","2026-01-12 18:46:05","TDIV","10,240573","262","RUB","0","MCX","","","False",""
"DIVIDEND","2026-01-14 11:09:42","T","0","36","RUB","5","MCX","","","False",""
"DIVIDEND","2026-01-20 15:55:25","TPAY","0","136,92","RUB","0","MCX","","","False",""
"CASH_IN","2026-02-02 11:12:04","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-02-02 11:32:35","RUB","1","5566","RUB","0","","","","False","Пополнение счета"
"BUY","2026-02-02 11:39:30","TBRU","7,8","302","RUB","0","MCX","","","False",""
"BUY","2026-02-02 11:39:30","TOFZ","13,57","767","RUB","0","MCX","","","False",""
"BUY","2026-02-02 11:39:30","TPAY","100,08","24","RUB","0","MCX","","","False",""
"SELL","2026-02-02 11:40:19","TOFZ","13,56","397","RUB","0","MCX","","","False",""
"CASH_IN","2026-02-05 14:25:59","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-02-09 11:24:18","TDIV","10,42","28","RUB","0","MCX","","","False",""
"BUY","2026-02-09 11:24:18","TMOS","6,43","279","RUB","0","MCX","","","False",""
"BUY","2026-02-09 11:24:18","TOFZ","13,53","22","RUB","0","MCX","","","False",""
"BUY","2026-02-09 11:24:19","SBRB","17,781","16","RUB","0,11","MCX","","","False",""
"BUY","2026-02-09 11:24:19","TBRU","7,8","153","RUB","0","MCX","","","False",""
"BUY","2026-02-09 11:24:19","TGLD","15,47","38","RUB","0","MCX","","","False",""
"BUY","2026-02-09 11:24:19","TPAY","100,19","11","RUB","0","MCX","","","False",""
"DIVIDEND","2026-02-09 16:36:01","TPAY","0","201,78","RUB","0","MCX","","","False",""
"BUY","2026-02-16 11:03:47","TDIV","10,69","1","RUB","0","MCX","","","False",""
"BUY","2026-02-16 11:03:47","TMOS","6,6","12","RUB","0","MCX","","","False",""
"BUY","2026-02-16 11:03:48","TBRU","7,9","6","RUB","0","MCX","","","False",""
"BUY","2026-02-16 11:03:48","TGLD","15,25","1","RUB","0","MCX","","","False",""
"SELL","2026-02-18 11:05:26","SBRB","17,917226","266","RUB","1,91","MCX","","","False",""
"BUY","2026-02-18 11:05:26","TMOS","6,56","6","RUB","0","MCX","","","False",""
"BUY","2026-02-18 11:05:26","TOFZ","13,82","380","RUB","0","MCX","","","False",""
"BUY","2026-02-18 11:05:27","TBRU","7,91","3","RUB","0","MCX","","","False",""
"SELL","2026-02-21 10:24:30","IVAT","159,55","20","RUB","1,28","MCX","","","False",""
"SELL","2026-02-21 10:24:40","ASTR","259,9","20","RUB","2,08","MCX","","","False",""
"TAX","2026-02-21 10:24:58","","0","0","RUB","32","","","","False","Удержание налога по ставке 13%"
"CASH_OUT","2026-02-21 10:24:58","RUB","1","8388,99","RUB","0","","","","False","Вывод со счета"
"CASH_IN","2026-02-24 00:00:00","RUB","1","0,0024","RUB","0","","","","False","Эта сделка добавлена с целью корректировки баланса по этой валюте, т.к. баланс по данным брокера (16,14) не соответствует балансу рассчитанному по сделкам (16,14)."
"CASH_IN","2026-02-24 00:00:00","RUB","1","2000","RUB","0","","","","False",""
"BUY","2026-02-24 11:31:18","LQDT","1,9291","1036","RUB","0","MCX","","","False",""
"TAX_RETURN","2026-02-26 02:13:39","","0","0","RUB","32","","","","False","Корректировка налога по ставке 13%"
"CASH_OUT","2026-03-01 18:17:42","RUB","1","32","RUB","0","","","","False","Вывод со счета"
"SELL","2026-03-02 11:31:12","TBRU","7,94","2051","RUB","0","MCX","","","False",""
"SELL","2026-03-02 11:31:12","TDIV","10,869698","463","RUB","0","MCX","","","False",""
"SELL","2026-03-02 11:31:12","TPAY","100,13","178","RUB","0","MCX","","","False",""
"SELL","2026-03-02 11:31:13","TGLD","16,69","303","RUB","0","MCX","","","False",""
"BUY","2026-03-02 11:31:13","TMOS","6,73","6144","RUB","0","MCX","","","False",""
"BUY","2026-03-02 11:31:14","TMOS","6,73","290","RUB","0","MCX","","","False",""
"BUY","2026-03-02 11:31:14","TOFZ","13,8","410","RUB","0","MCX","","","False",""
"BUY","2026-03-02 11:31:15","TBRU","7,95","17","RUB","0","MCX","","","False",""
"CASH_IN","2026-03-05 14:48:54","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-03-09 11:01:40","TMOS","6,85","283","RUB","0","MCX","","","False",""
"BUY","2026-03-09 11:01:40","TOFZ","13,87","29","RUB","0","MCX","","","False",""
"BUY","2026-03-09 11:01:41","TBRU","7,97","17","RUB","0","MCX","","","False",""
"BUY","2026-03-09 11:01:41","TGLD","15,9","8","RUB","0","MCX","","","False",""
"DIVIDEND","2026-03-10 16:55:28","TPAY","0","199,98","RUB","0","MCX","","","False",""
"BUY","2026-03-16 11:35:58","TMOS","6,81","32","RUB","0","MCX","","","False",""
"BUY","2026-03-16 11:35:59","TBRU","8,07","1","RUB","0","MCX","","","False",""
"BUY","2026-03-16 11:35:59","TOFZ","14,06","3","RUB","0","MCX","","","False",""
"BUY","2026-03-23 11:11:19","TMOS","6,76","7","RUB","0","MCX","","","False",""
"BUY","2026-03-30 11:53:07","TMOS","6,56","7","RUB","0","MCX","","","False",""
"CASH_IN","2026-04-01 14:43:30","RUB","1","1100","RUB","0","","","","False","Пополнение счета"
"BUY","2026-04-01 14:47:55","RU000A10BF48","1008","1","RUB","3,02","MCX","7,46","","False",""
"CASH_IN","2026-04-01 20:05:46","RUB","1","100000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-04-01 20:08:22","SU26207RMFS9","964,82","8","RUB","23,16","MCX","12,73","","False",""
"BUY","2026-04-01 20:08:40","SU26207RMFS9","964,81","8","RUB","23,16","MCX","12,73","","False",""
"BUY","2026-04-01 20:09:02","SU26207RMFS9","964,84","14","RUB","40,52","MCX","12,73","","False",""
"BUY","2026-04-01 20:09:33","SU29016RMFS1","1003,88","10","RUB","30,11","MCX","3,24","","False",""
"BUY","2026-04-01 20:09:47","SU29007RMFS0","1020","10","RUB","30,6","MCX","13,89","","False",""
"BUY","2026-04-01 20:18:20","RU000A10AT35","1029,1","5","RUB","15,44","MCX","6,62","","False",""
"BUY","2026-04-01 20:18:33","RU000A10BF48","1008","4","RUB","12,1","MCX","7,46","","False",""
"BUY","2026-04-01 20:21:25","RU000A10CQ85","999,2","5","RUB","14,99","MCX","8,44","","False",""
"BUY","2026-04-01 20:22:09","RU000A10B0U0","1070,5","5","RUB","16,06","MCX","1,26","","False",""
"BUY","2026-04-01 20:23:59","RU000A10B214","1042,4","5","RUB","15,64","MCX","13,36","","False",""
"CUSTOM_HOLDING_PRICE","2026-04-03 00:00:00","SIBN6P4","11353,38392","0","RUB","0","CUSTOM_HOLDING","0","","",""
"BUY","2026-04-03 16:38:22","SIBN6P4","11353,38392","2","RUB","68,12","CUSTOM_HOLDING","25,35108","","False",""
"CASH_IN","2026-04-05 14:36:58","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-04-06 11:24:08","TMOS","6,49","558","RUB","0","MCX","","","False",""
"BUY","2026-04-06 11:24:09","TMOS","6,49","543","RUB","0","MCX","","","False",""
"BUY","2026-04-06 11:24:10","TPAY","100,92","1","RUB","0","MCX","","","False",""
"SELL","2026-04-06 11:51:39","TMOS","6,48","539","RUB","0","MCX","","","False",""
"DIVIDEND","2026-04-07 15:55:02","RU000A10B214","0","77,05","RUB","0","MCX","","","False",""
"CASH_IN","2026-04-08 00:00:00","RUB","1","3000","RUB","0","","","","False",""
"BUY","2026-04-08 15:24:35","LQDT","1,9629","1529","RUB","0","MCX","","","False",""
"DIVIDEND","2026-04-09 13:13:04","TPAY","0","33,01","RUB","0","MCX","","","False",""
"CASH_IN","2026-04-11 21:18:45","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-04-12 10:34:35","SU26207RMFS9","966,13","5","RUB","14,49","MCX","15,41","","False",""
"CASH_IN","2026-04-12 10:46:14","RUB","1","10000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-04-12 10:46:24","SU29007RMFS0","1020,5","5","RUB","15,31","MCX","19,63","","False",""
"BUY","2026-04-12 10:46:32","SU29016RMFS1","1001,35","5","RUB","15,02","MCX","8,13","","False",""
"BUY","2026-04-13 12:54:35","TBRU","8,16","7","RUB","0","MCX","","","False",""
"BUY","2026-04-13 12:54:35","TGLD","14,24","4","RUB","0","MCX","","","False",""
"BUY","2026-04-13 12:54:35","TMOS","6,45","136","RUB","0","MCX","","","False",""
"BUY","2026-04-13 12:54:35","TOFZ","14,09","13","RUB","0","MCX","","","False",""
"DIVIDEND","2026-04-13 13:44:05","RU000A10CQ85","0","57,55","RUB","0","MCX","","","False",""
"CASH_IN","2026-04-15 22:54:40","RUB","1","1200","RUB","0","","","","False","Пополнение счета"
"SPLIT","2026-04-16 03:00:00","T","10","0","RUB","0","MCX","0","","",""
"SPLIT","2026-04-16 03:00:00","T","10","0","RUB","0","MCX","0","","",""
"CASH_IN","2026-04-16 14:08:35","RUB","1","1100","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-04-16 14:08:36","RUB","1","1100","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-04-17 00:00:00","RUB","1","1100","RUB","0","","","","False",""
"CASH_IN","2026-04-17 00:00:00","RUB","1","1110","RUB","0","","","","False","ЗАЧИСЛЕНИЕ Д/С"
"CASH_OUT","2026-04-17 00:00:00","RUB","1","1017,83999543","RUB","0","","","","False","Эта сделка добавлена с целью корректировки баланса по этой валюте, т.к. баланс по данным брокера (357,15) не соответствует балансу рассчитанному по сделкам (1374,99)."
"BUY","2026-04-17 00:00:00","STME","4,285","262","RUB","0,22","MCX","","","False",""
"DIVIDEND","2026-04-17 13:25:18","RU000A10BF48","0","69,3","RUB","0","MCX","","","False",""
"BUY","2026-04-17 15:04:10","LQDT","1,9714","558","RUB","0","MCX","","","False",""
"CASH_IN","2026-04-20 00:00:00","RUB","1","3458,88","RUB","0","","","","False","ПЕРЕВОД Д/С С ДОГОВОРА 8184V30, ТП ОСНОВНОЙ РЫНОК НА ДОГОВОР S930W42, ТП ОСНОВНОЙ РЫНОК"
"BUY","2026-04-20 12:23:33","TMOS","6,45629","124","RUB","0","MCX","","","False",""
"BUY","2026-04-20 12:23:34","TBRU","8,21","6","RUB","0","MCX","","","False",""
"BUY","2026-04-20 12:23:34","TGLD","14,35","4","RUB","0","MCX","","","False",""
"BUY","2026-04-20 12:23:34","TOFZ","14,24","12","RUB","0","MCX","","","False",""
"BUY","2026-04-20 12:33:50","RU000A10BF48","1008,2","1","RUB","3,02","MCX","2,29","","False",""
"BUY","2026-04-20 12:34:00","RU000A10B214","1046,6","1","RUB","3,14","MCX","7,71","","False",""
"BUY","2026-04-20 12:34:08","RU000A10CQ85","999,9","1","RUB","3","MCX","4,22","","False",""
"BUY","2026-04-20 12:34:16","RU000A10AT35","1026,6","1","RUB","3,08","MCX","1,42","","False",""
"DIVIDEND","2026-04-20 17:33:41","RU000A10AT35","0","70,9","RUB","0","MCX","","","False",""
"BUY","2026-04-22 00:00:00","SBRB","18,6130117","342","RUB","1,27","MCX","","","False",""
"SELL","2026-04-22 00:00:00","STME","4,24500713","701","RUB","0,6","MCX","","","False",""
"SELL","2026-04-22 15:04:08","TPAY","101,28","34","RUB","0","MCX","","","False",""
"BUY","2026-04-22 15:04:09","TBRU","8,24","2115","RUB","0","MCX","","","False",""
"SELL","2026-04-22 15:04:09","TOFZ","14,28","1239","RUB","0","MCX","","","False",""
"BUY","2026-04-22 15:04:10","TGLD","14,17","320","RUB","0","MCX","","","False",""
"BUY","2026-04-22 15:04:10","TMOS","6,51","3","RUB","0","MCX","","","False",""
"SELL","2026-04-22 15:22:42","LQDT","1,9735","3123","RUB","0","MCX","","","False",""
"BUY","2026-04-22 15:23:14","EQMX","143,2","43","RUB","1,24","MCX","","","False",""
"CASH_IN","2026-04-28 00:00:00","RUB","1","4000","RUB","0","","","","False",""
"CASH_IN","2026-04-28 00:00:00","RUB","1","4000,81","RUB","0","","","","False","ЗАЧИСЛЕНИЕ Д/С"
"BUY","2026-04-28 00:00:00","SBRB","18,68601852","216","RUB","0,8","MCX","","","False",""
"BUY","2026-04-28 11:10:13","EQMX","141,6","28","RUB","0,8","MCX","","","False",""
"DIVIDEND","2026-04-28 16:28:09","SIBN6P4","0","131,44","RUB","0","CUSTOM_HOLDING","","","False",""
"CASH_IN","2026-05-01 00:16:15","RUB","1","760","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2026-05-04 13:13:02","RU000A10B0U0","0","94,5","RUB","0","MCX","","","False",""
"CASH_IN","2026-05-05 13:48:20","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2026-05-07 11:34:21","RU000A10B214","0","92,46","RUB","0","MCX","","","False",""
"CASH_IN","2026-05-10 10:27:31","RUB","1","2000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-05-11 13:45:38","TMOS","6,36","485","RUB","0","MCX","","","False",""
"BUY","2026-05-11 13:45:39","TGLD","13,6","24","RUB","0","MCX","","","False",""
"BUY","2026-05-11 13:45:39","TMOS","6,36","464","RUB","0","MCX","","","False",""
"CASH_IN","2026-05-12 16:23:54","RUB","1","9000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-05-12 16:26:57","RU000A10B0U0","1066,7","5","RUB","16","MCX","8,19","","False",""
"BUY","2026-05-12 16:27:09","RU000A10AT35","1031,5","4","RUB","12,38","MCX","11,82","","False",""
"BUY","2026-05-12 16:27:44","RU000A10BF48","1008","2","RUB","6,05","MCX","12,19","","False",""
"DIVIDEND","2026-05-13 12:22:10","RU000A10CQ85","0","69,06","RUB","0","MCX","","","False",""
"SELL","2026-05-18 14:13:35","TMOS","6,3","488","RUB","0","MCX","","","False",""
"DIVIDEND","2026-05-18 17:46:19","RU000A10AT35","0","141,8","RUB","0","MCX","","","False",""
"DIVIDEND","2026-05-19 12:28:16","RU000A10BF48","0","108,16","RUB","0","MCX","","","False",""
"BUY","2026-05-25 14:33:45","TMOS","6,28","98","RUB","0","MCX","","","False",""
"BUY","2026-05-25 14:33:46","TBRU","8,32","21","RUB","0","MCX","","","False",""
"BUY","2026-05-25 14:33:46","TGLD","12,81","5","RUB","0","MCX","","","False",""
"DIVIDEND","2026-05-26 18:44:26","SIBN6P4","0","124,94","RUB","0","CUSTOM_HOLDING","","","False",""
"CASH_IN","2026-05-28 00:00:00","RUB","1","4000","RUB","0","","","","False",""
"CASH_IN","2026-05-28 00:00:00","RUB","1","4000","RUB","0","","","","False","ЗАЧИСЛЕНИЕ Д/С"
"BUY","2026-05-28 00:00:00","SBRB","18,89099057","212","RUB","0,8","MCX","","","False",""
"BUY","2026-05-28 11:06:55","EQMX","136,3","29","RUB","0,79","MCX","","","False",""
"BUY","2026-06-01 16:08:51","TBRU","8,33","1","RUB","0","MCX","","","False",""
"BUY","2026-06-01 16:08:51","TMOS","6,15","5","RUB","0","MCX","","","False",""
"DIVIDEND","2026-06-02 12:30:21","RU000A10B0U0","0","189","RUB","0","MCX","","","False",""
"CASH_IN","2026-06-05 12:10:51","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2026-06-08 12:41:38","RU000A10B214","0","92,46","RUB","0","MCX","","","False",""
"BUY","2026-06-08 14:44:52","TMOS","6,1","588","RUB","0","MCX","","","False",""
"BUY","2026-06-08 14:44:52","TMOS","6,1","576","RUB","0","MCX","","","False",""
"SELL","2026-06-08 14:47:22","TMOS","6,09","569","RUB","0","MCX","","","False",""
"BUY","2026-06-08 15:13:02","TMOS","6,09","151","RUB","0","MCX","","","False",""
"BUY","2026-06-08 15:13:03","TBRU","8,36","31","RUB","0","MCX","","","False",""
"BUY","2026-06-08 15:13:03","TGLD","12,48","8","RUB","0","MCX","","","False",""
"DIVIDEND","2026-06-10 13:22:42","RU000A10CQ85","0","69,06","RUB","0","MCX","","","False",""
"BUY","2026-06-15 16:28:53","TMOS","6,06","3","RUB","0","MCX","","","False",""
"CASH_IN","2026-06-15 19:47:02","RUB","1","10000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-06-15 20:22:47","RU000A10B214","1039,9","4","RUB","12,48","MCX","5,65","","False",""
"BUY","2026-06-15 20:22:56","RU000A10BF48","1008","2","RUB","6,05","MCX","0,44","","False",""
"BUY","2026-06-15 20:23:06","RU000A10CQ85","1004,2","4","RUB","12,05","MCX","2,68","","False",""
"DIVIDEND","2026-06-16 15:47:20","RU000A10BF48","0","106,56","RUB","0","MCX","","","False",""
"DIVIDEND","2026-06-18 10:10:16","RU000A10AT35","0","141,8","RUB","0","MCX","","","False",""
"BUY","2026-06-22 16:08:34","TBRU","8,28","3","RUB","0","MCX","","","False",""
"BUY","2026-06-22 16:08:34","TMOS","5,63","17","RUB","0","MCX","","","False",""
"DIVIDEND","2026-06-24 16:34:47","SU29016RMFS1","0","538,05","RUB","0","MCX","","","False",""
"DIVIDEND","2026-06-25 19:11:14","SIBN6P4","0","131,06","RUB","0","CUSTOM_HOLDING","","","False",""
"BUY","2026-06-26 23:28:57","TMOS","5,51","417","RUB","0","MCX","","","False",""
"CASH_IN","2026-06-28 00:00:00","RUB","1","4000","RUB","0","","","","False",""
"CASH_IN","2026-06-28 00:00:00","RUB","1","4000","RUB","0","","","","False","ЗАЧИСЛЕНИЕ Д/С"
"BUY","2026-06-28 00:00:00","SBRB","18,76201878","213","RUB","0,8","MCX","","","False",""
"BUY","2026-06-28 14:13:46","EQMX","121,6","33","RUB","0","MCX","","","False",""
"DIVIDEND","2026-06-30 12:00:37","RU000A10B0U0","0","189","RUB","0","MCX","","","False",""
"CASH_IN","2026-07-01 12:25:32","RUB","1","1000","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-07-01 12:25:56","RUB","1","1000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-07-01 12:39:46","TGLD","12,17","68","RUB","0","MCX","","","False",""
"DIVIDEND","2026-07-07 13:30:58","RU000A10B214","0","154,1","RUB","0","MCX","","","False",""
"DIVIDEND","2026-07-10 10:36:35","RU000A10CQ85","0","115,1","RUB","0","MCX","","","False",""
"BUY","2026-07-10 10:43:57","RU000A10CQ85","997,1","1","RUB","0,4","MCX","1,53","","False",""
"BUY","2026-07-10 10:44:04","RU000A10CQ85","997,2","2","RUB","0,8","MCX","1,53","","False",""
"CASH_IN","2026-07-13 11:03:09","RUB","1","10000","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-07-13 11:03:15","RUB","1","10000","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-07-13 11:37:10","RUB","1","5000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-07-13 12:32:18","TBRU","8,21","217","RUB","0","MCX","","","False",""
"BUY","2026-07-13 12:32:18","TMOS","5,29","1523","RUB","0","MCX","","","False",""
"BUY","2026-07-13 12:32:19","TGLD","12,23","58","RUB","0","MCX","","","False",""
"BUY","2026-07-13 12:33:16","RU000A10B0U0","1044,2","3","RUB","1,25","MCX","9,45","","False",""
"BUY","2026-07-14 07:18:33","RU000A10B0U0","1045","2","RUB","0,84","MCX","10,08","","False",""
"BUY","2026-07-14 07:18:48","RU000A10CQ85","998,3","2","RUB","0,8","MCX","2,3","","False",""
"BUY","2026-07-14 07:18:57","RU000A10AT35","1026,2","5","RUB","2,05","MCX","13,23","","False",""
"BUY","2026-07-14 07:19:14","SU29016RMFS1","1000,96","2","RUB","0,8","MCX","8,13","","False",""
"DIVIDEND","2026-07-16 12:32:09","RU000A10BF48","0","131,8","RUB","0","MCX","","","False",""
"DIVIDEND","2026-07-17 17:42:53","RU000A10AT35","0","212,7","RUB","0","MCX","","","False",""
"BUY","2026-07-19 15:39:27","TMOS","4,77","130","RUB","0","MCX","","","False",""
"SELL","2026-07-20 13:22:33","TBRU","8,06","290","RUB","0","MCX","","","False",""
"CASH_IN","2026-07-23 00:00:00","RUB","1","10000","RUB","0","","","","False","ЗАЧИСЛЕНИЕ Д/С"
"BUY","2026-07-23 00:00:00","SBER","263,68","17","RUB","13,45","MCX","","","False",""
"BUY","2026-07-23 00:00:00","SBER","263,68","3","RUB","2,37","MCX","","","False",""
"BUY","2026-07-24 00:00:00","MOEX","146,28","10","RUB","4,39","MCX","","","False",""
"BUY","2026-07-24 00:00:00","MOEX","146,28","20","RUB","8,78","MCX","","","False",""
"DIVIDEND","2026-07-28 09:53:12","SIBN6P4","0","138,06","RUB","0","CUSTOM_HOLDING","","","False",""
"DIVIDEND","2026-07-30 11:50:58","RU000A10B0U0","0","283,5","RUB","0","MCX","","","False",""
"BUY","2026-07-30 12:13:11","SU26207RMFS9","978,99","1","RUB","2,94","MCX","39,52","","False",""
"DIVIDEND","2026-08-05 13:31:58","RU000A10B214","0","154,1","RUB","0","MCX","","","False",""
"DIVIDEND","2026-08-05 18:29:11","SU26207RMFS9","0","1463,04","RUB","0","MCX","","","False",""
"BUY","2026-08-05 22:37:13","SU26207RMFS9","978,88","1","RUB","2,94","MCX","0,22","","False",""
"DIVIDEND","2026-08-11 13:40:31","RU000A10CQ85","0","172,65","RUB","0","MCX","","","False",""
"BUY","2026-08-13 00:00:00","LKOH","4534,5","1","RUB","14,96","MCX","","","False",""
"CASH_IN","2026-08-13 00:00:00","RUB","1","4000","RUB","0","","","","False",""
"CASH_IN","2026-08-13 00:00:00","RUB","1","10551,18","RUB","0","","","","False","ПЕРЕВОД Д/С С ДОГОВОРА 8184V30, ТП ОСНОВНОЙ РЫНОК НА ДОГОВОР S930W42, ТП ОСНОВНОЙ РЫНОК"
"BUY","2026-08-13 00:00:00","TATN","548,3","9","RUB","16,29","MCX","","","False",""
"CASH_IN","2026-08-13 11:05:50","RUB","1","14000","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-08-13 13:06:52","RUB","1","10000","RUB","0","","","","False","Пополнение счета"
"BUY","2026-08-13 15:56:15","EQMX","122,9","33","RUB","0,81","MCX","","","False",""
"BUY","2026-08-14 14:13:20","SU29016RMFS1","1000,8","5","RUB","15,01","MCX","20,92","","False",""
"BUY","2026-08-14 14:13:42","SU26207RMFS9","979,08","6","RUB","17,62","MCX","2,68","","False",""
"DIVIDEND","2026-08-17 13:13:29","RU000A10BF48","0","130,1","RUB","0","MCX","","","False",""
"DIVIDEND","2026-08-17 16:37:48","RU000A10AT35","0","212,7","RUB","0","MCX","","","False",""
"BUY","2026-08-18 14:52:52","TBRU","8,41","427","RUB","0","MCX","","","False",""
"BUY","2026-08-18 14:52:52","TMOS","5,34","1843","RUB","0","MCX","","","False",""
"DIVIDEND","2026-08-26 09:18:46","SIBN6P4","0","147,02","RUB","0","CUSTOM_HOLDING","","","False",""
"CASH_IN","2026-08-27 11:04:19","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"BUY","2026-08-27 11:04:52","TMOS","5,16","121","RUB","0","MCX","","","False",""
"CASH_IN","2026-08-28 11:45:37","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-08-29 12:12:09","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-08-30 12:07:12","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"BUY","2026-08-31 11:00:16","TBRU","8,44","40","RUB","0","MCX","","","False",""
"BUY","2026-08-31 11:00:16","TMOS","5,33","173","RUB","0","MCX","","","False",""
"DIVIDEND","2026-08-31 12:29:47","RU000A10B0U0","0","283,5","RUB","0","MCX","","","False",""
"CASH_IN","2026-08-31 13:44:09","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"BUY","2026-09-01 11:00:11","TBRU","8,46","25","RUB","0","MCX","","","False",""
"BUY","2026-09-01 11:00:11","TMOS","5,43","38","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-01 12:49:47","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"BUY","2026-09-02 11:00:17","TBRU","8,45","18","RUB","0","MCX","","","False",""
"BUY","2026-09-02 11:00:17","TMOS","5,4","56","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-02 14:10:10","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2026-09-03 10:57:34","SU29007RMFS0","0","1307,4","RUB","0","MCX","","","False",""
"BUY","2026-09-03 11:00:18","TBRU","8,45","113","RUB","0","MCX","","","False",""
"BUY","2026-09-03 11:00:18","TMOS","5,45","425","RUB","0","MCX","","","False",""
"BUY","2026-09-03 11:20:02","SU26207RMFS9","980,48","2","RUB","5,88","MCX","6,7","","False",""
"CASH_IN","2026-09-03 13:52:30","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"BUY","2026-09-04 11:00:21","TMOS","5,58","9","RUB","0","MCX","","","False",""
"BUY","2026-09-04 11:00:22","TBRU","8,45","50","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-04 11:31:06","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2026-09-04 12:06:55","RU000A10B214","0","154,1","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-05 13:46:10","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-09-06 14:12:11","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"BUY","2026-09-07 11:00:18","TBRU","8,46","66","RUB","0","MCX","","","False",""
"BUY","2026-09-07 11:00:18","TMOS","5,58","140","RUB","0","MCX","","","False",""
"BUY","2026-09-08 11:00:20","TBRU","8,46","2","RUB","0","MCX","","","False",""
"DIVIDEND","2026-09-08 13:02:22","RU000A10CQ85","0","172,65","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-11 16:30:24","RUB","1","2300","RUB","0","","","","False","Пополнение счета"
"BUY","2026-09-11 18:32:59","TBRU","8,47","97","RUB","0","MCX","","","False",""
"BUY","2026-09-11 18:32:59","TGLD","14,32","41","RUB","0","MCX","","","False",""
"BUY","2026-09-11 18:32:59","TMOS","5,69","151","RUB","0","MCX","","","False",""
"CUSTOM_HOLDING_PRICE","2026-09-12 00:00:00","SIBN6P4","12228,06098","0","RUB","0","CUSTOM_HOLDING","0","","",""
"CASH_IN","2026-09-12 11:31:49","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"CUSTOM_HOLDING_PRICE","2026-09-13 00:00:00","SIBN6P4","12228,06098","0","RUB","0","CUSTOM_HOLDING","0","","",""
"CASH_IN","2026-09-13 13:32:57","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"CASH_IN","2026-09-13 14:37:16","RUB","1","10000","RUB","0","","","","False","Пополнение счета"
"CUSTOM_HOLDING_PRICE","2026-09-14 00:00:00","SIBN6P4","12320,94504","0","RUB","0","CUSTOM_HOLDING","0","","",""
"BUY","2026-09-14 11:00:21","TBRU","8,48","60","RUB","0","MCX","","","False",""
"BUY","2026-09-14 11:00:21","TMOS","5,78","10","RUB","0","MCX","","","False",""
"BUY","2026-09-14 11:00:22","TGLD","14,15","25","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-14 14:16:05","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"CUSTOM_HOLDING_PRICE","2026-09-15 00:00:00","SIBN6P4","12284,594","0","RUB","0","CUSTOM_HOLDING","0","","",""
"BUY","2026-09-15 11:00:18","TBRU","8,49","39","RUB","0","MCX","","","False",""
"BUY","2026-09-15 11:00:18","TGLD","14,2","8","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-15 14:34:11","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"DIVIDEND","2026-09-15 15:56:39","RU000A10BF48","0","129","RUB","0","MCX","","","False",""
"DIVIDEND","2026-09-15 16:51:49","RU000A10AT35","0","212,7","RUB","0","MCX","","","False",""
"BUY","2026-09-16 00:00:00","AFLT","32,65","30","RUB","3,22","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","AFLT","32,66","100","RUB","10,78","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","CHMF","638,4","2","RUB","4,22","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","CHMF","639,4","5","RUB","10,55","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","MSNG","1,44","1000","RUB","4,75","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","MSNG","1,44","2000","RUB","9,49","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","MTSS","189,8","2","RUB","1,26","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","MTSS","189,1","10","RUB","6,25","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","MTSS","190,25","5","RUB","3,14","MCX","0","RUB","False",""
"BUY","2026-09-16 00:00:00","PHOR","5309","2","RUB","35,05","MCX","0","RUB","False",""
"CASH_IN","2026-09-16 00:00:00","RUB","1","10000","RUB","0","","","","False",""
"SELL","2026-09-16 00:00:00","SBRB","19,06","983","RUB","3,75","MCX","0","RUB","False",""
"BUY","2026-09-16 11:00:22","TMOS","5,8","80","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-16 11:31:47","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"CUSTOM_HOLDING_SETTINGS","2026-09-17 00:00:00","SIBN6P4","0","0","RUB","0","CUSTOM_HOLDING","0","","","{@*@Holding@*@:{@*@Note@*@:null,@*@Currency@*@:@*@RUB@*@,@*@Description@*@:@*@\u0413\u0430\u0437\u043F\u0440\u043E\u043C \u041D\u0435\u0444\u0442\u044C 006\u0420-04@*@,@*@DividendTax@*@:null,@*@Sector@*@:@*@Other@*@},@*@Settings@*@:{@*@CustomHoldingType@*@:0,@*@IncomeType@*@:1,@*@FirstIncomeDate@*@:null,@*@GenerateIncome@*@:false,@*@IncomeAmount@*@:null,@*@IncomeReinvestmentType@*@:0,@*@IsIncomeReinvested@*@:false,@*@MaturityDate@*@:null,@*@Period@*@:null,@*@PeriodType@*@:0,@*@NextIncomeDate@*@:null}}"
"BUY","2026-09-17 11:00:21","TMOS","5,61","80","RUB","0","MCX","","","False",""
"CASH_IN","2026-09-17 11:29:23","RUB","1","450","RUB","0","","","","False","Пополнение счета"
"BUY","2026-09-17 16:23:00","RU000A10BK09","1014,4","2","RUB","6,09","MCX","8,33","","False",""
"BUY","2026-09-17 16:23:11","RU000A10BK09","1014,4","3","RUB","9,13","MCX","8,33","","False",""
"BUY","2026-09-17 16:23:18","RU000A10BK09","1014,5","1","RUB","3,04","MCX","8,33","","False",""
"BUY","2026-09-17 16:23:50","RU000A10BF48","1006","4","RUB","12,07","MCX","2,15","","False",""
"SELL","2026-09-17 16:24:21","RU000A10CQ85","1000,2","9","RUB","27","MCX","4,22","","False",""
"SELL","2026-09-17 16:24:26","RU000A10CQ85","1000,1","1","RUB","3","MCX","4,22","","False",""
"SELL","2026-09-17 16:24:30","RU000A10CQ85","1000","5","RUB","15","MCX","4,22","","False",""
"BUY","2026-09-17 16:24:46","RU000A10BK09","1014,4","7","RUB","21,3","MCX","8,33","","False",""
"BUY","2026-09-17 16:24:54","RU000A10BF48","1006","8","RUB","24,14","MCX","2,15","","False",""
1 Event Date Symbol Price Quantity Currency FeeTax Exchange NKD FeeCurrency DoNotAdjustCash Note
2 CASH_IN 2025-02-26 09:22:54 RUB 1 5000 RUB 0 False Пополнение счета
3 CASH_OUT 2025-02-26 09:22:54 RUB 1 0,002832 RUB 0 False Эта сделка добавлена с целью корректировки баланса по этой валюте, т.к. баланс по данным брокера (459,38) не соответствует балансу рассчитанному по сделкам (459,38).
4 BUY 2025-02-26 09:23:29 DELI 219 10 RUB 6,57 MCX False
5 BUY 2025-02-26 09:24:09 ROSN 581,25 2 RUB 3,49 MCX False
6 BUY 2025-02-26 09:25:23 DELI 219 7 RUB 4,6 MCX False
7 CASH_IN 2025-02-27 19:28:34 RUB 1 3400 RUB 0 False Пополнение счета
8 BUY 2025-02-27 19:28:47 SBERP 307,39 10 RUB 9,22 MCX False
9 BUY 2025-02-28 08:49:02 DELI 211,45 1 RUB 0,63 MCX False
10 CASH_IN 2025-03-12 16:12:53 RUB 1 10000 RUB 0 False Пополнение счета
11 BUY 2025-03-12 16:13:39 DELI 196,3 12 RUB 7,07 MCX False
12 BUY 2025-03-12 16:13:49 DELI 196,3 10 RUB 5,89 MCX False
13 BUY 2025-03-12 16:14:20 DELI 196,4 10 RUB 5,89 MCX False
14 BUY 2025-03-12 16:18:28 RU000A107AM4 929,25 4 RUB 11,15 MCX 5,08 False
15 CASH_IN 2025-03-12 16:19:39 RUB 1 197,7 RUB 0 False Пополнение счета
16 BUY 2025-03-12 16:19:40 DELI 196,5 1 RUB 0,59 MCX False
17 BUY 2025-03-24 16:28:49 ASTR 427,45 2 RUB 2,56 MCX False
18 CASH_IN 2025-03-24 16:28:49 RUB 1 860,08 RUB 0 False Пополнение счета
19 CASH_IN 2025-03-25 18:00:22 RUB 1 5007,5 RUB 0 False Пополнение счета
20 CASH_IN 2025-03-25 18:02:09 RUB 1 5508,25 RUB 0 False Пополнение счета
21 BUY 2025-03-25 18:17:42 WUSH 217,552 5 RUB 3,26 MCX False
22 BUY 2025-03-25 18:21:55 TATNP 647,5 2 RUB 3,89 MCX False
23 BUY 2025-03-25 18:24:32 SFIN 1538,8 1 RUB 4,62 MCX False
24 BUY 2025-03-25 18:24:41 LENT 1384 1 RUB 4,15 MCX False
25 BUY 2025-03-25 18:25:23 IVAT 187,5 1 RUB 0,56 MCX False
26 BUY 2025-03-26 17:27:00 TPAY 96,5 1 RUB 0 MCX False
27 BUY 2025-04-02 11:22:50 RU000A10B7T7 1000 5 RUB 7,5 MCX 0 False
28 DIVIDEND 2025-04-09 18:40:43 TPAY 0 1,94 RUB 0 MCX False
29 CASH_IN 2025-04-11 18:24:33 RUB 1 8745,17 RUB 0 False Пополнение счета
30 CASH_OUT 2025-04-11 18:24:33 RUB 1 0,010799 RUB 0 False Эта сделка добавлена с целью корректировки баланса по этой валюте, т.к. баланс по данным брокера (364,86) не соответствует балансу рассчитанному по сделкам (364,87).
31 BUY 2025-04-11 18:25:10 WUSH 181,58 10 RUB 5,45 MCX False
32 BUY 2025-04-11 18:25:27 IVAT 152,99 10 RUB 4,59 MCX False
33 BUY 2025-04-11 18:25:58 ROSN 450,566667 3 RUB 4,06 MCX False
34 BUY 2025-04-11 18:26:54 PIKK 427,3 3 RUB 3,85 MCX False
35 BUY 2025-04-11 18:28:10 CNRU 539,8 3 RUB 4,86 MCX False
36 BUY 2025-04-18 09:52:48 ROSN 475,05 2 RUB 2,85 MCX False
37 CASH_IN 2025-04-25 16:11:11 RUB 1 1626,6 RUB 0 False Пополнение счета
38 CASH_IN 2025-04-25 16:11:25 RUB 1 1626,6 RUB 0 False Пополнение счета
39 BUY 2025-04-28 10:34:11 TPAY 96,76 17 RUB 0 MCX False
40 BUY 2025-04-28 10:35:25 NVTK 1259,3 1 RUB 3,78 MCX False
41 CASH_IN 2025-04-29 19:39:31 RUB 1 2610 RUB 0 False Пополнение счета
42 CASH_IN 2025-04-29 19:39:41 RUB 1 2610 RUB 0 False Пополнение счета
43 BUY 2025-04-30 10:50:36 TPAY 95,47 2 RUB 0 MCX False
44 BUY 2025-04-30 10:50:48 RU000A107AM4 935,8 1 RUB 2,81 MCX 23,23 False
45 BUY 2025-04-30 10:51:05 DELI 169,4 8 RUB 4,07 MCX False
46 BUY 2025-04-30 10:51:20 CNRU 549 2 RUB 3,29 MCX False
47 BUY 2025-04-30 10:51:32 PIKK 470,8 2 RUB 2,82 MCX False
48 BUY 2025-04-30 10:52:36 APTK 10,412 100 RUB 3,12 MCX False
49 CASH_IN 2025-05-12 19:51:08 RUB 1 5000 RUB 0 False Пополнение счета
50 CASH_IN 2025-05-12 19:51:19 RUB 1 5000 RUB 0 False Пополнение счета
51 BUY 2025-05-14 10:43:57 EUTR 125,25 10 RUB 3,76 MCX False
52 BUY 2025-05-14 10:45:03 DELI 163,6 1 RUB 0,49 MCX False
53 BUY 2025-05-14 10:45:16 ROSN 451,2 8 RUB 10,83 MCX False
54 BUY 2025-05-14 10:45:36 PIKK 490,58 5 RUB 7,36 MCX False
55 BUY 2025-05-14 10:46:01 NVTK 1192,55 2 RUB 7,16 MCX False
56 DIVIDEND 2025-05-15 17:05:35 TPAY 0 25,72 RUB 0 MCX False
57 CASH_IN 2025-05-23 16:12:44 RUB 1 1200 RUB 0 False Пополнение счета
58 CASH_IN 2025-05-23 16:12:54 RUB 1 1200 RUB 0 False Пополнение счета
59 BUY 2025-05-27 08:55:30 DELI 157,65 8 RUB 3,78 MCX False
60 BUY 2025-05-27 08:55:47 DELI 157,65 8 RUB 3,78 MCX False
61 DIVIDEND 2025-05-30 09:12:44 RU000A107AM4 0 165,15 RUB 0 MCX False
62 CASH_IN 2025-06-05 12:46:14 RUB 1 2600 RUB 0 False Пополнение счета
63 CASH_IN 2025-06-05 12:46:24 RUB 1 2600 RUB 0 False Пополнение счета
64 BUY 2025-06-05 16:21:32 RU000A10B7T7 1038 2 RUB 6,23 MCX 44,52 False
65 BUY 2025-06-05 16:21:43 DELI 169,85 2 RUB 1,02 MCX False
66 BUY 2025-06-05 16:21:53 NVTK 1110,55 2 RUB 6,66 MCX False
67 BUY 2025-06-05 16:22:12 IVAT 128,35 5 RUB 1,93 MCX False
68 DIVIDEND 2025-06-09 15:56:44 TPAY 0 26,82 RUB 0 MCX False
69 DIVIDEND 2025-06-11 12:29:50 SFIN 0 83,5 RUB 9 MCX False
70 CASH_IN 2025-06-12 10:19:15 RUB 1 6000 RUB 0 False Пополнение счета
71 CASH_IN 2025-06-12 10:19:29 RUB 1 6000 RUB 0 False Пополнение счета
72 DIVIDEND 2025-06-19 15:21:06 TATNP 0 86,22 RUB 11 MCX False
73 BUY 2025-06-19 16:22:31 TMOS 6,21 398 RUB 0 MCX False
74 BUY 2025-06-19 16:23:05 TMOS 6,21 398 RUB 0 MCX False
75 BUY 2025-06-19 16:24:05 SBRB 15,845 77 RUB 3,66 MCX False
76 BUY 2025-06-19 16:24:16 SBRB 15,845 77 RUB 3,66 MCX False
77 BUY 2025-06-19 16:25:10 TGLD 10,52 58 RUB 0 MCX False
78 BUY 2025-06-19 16:25:27 TGLD 10,52 58 RUB 0 MCX False
79 BUY 2025-06-19 16:26:09 LQDT 1,7196 356 RUB 1,84 MCX False
80 BUY 2025-06-19 16:26:26 LQDT 1,7196 356 RUB 1,84 MCX False
81 BUY 2025-06-19 16:27:03 TPAY 98,23 12 RUB 0 MCX False
82 BUY 2025-06-19 16:27:09 TPAY 98,23 12 RUB 0 MCX False
83 CASH_IN 2025-06-25 19:07:20 RUB 1 1200 RUB 0 False Пополнение счета
84 CASH_IN 2025-06-25 19:07:31 RUB 1 1200 RUB 0 False Пополнение счета
85 AMORTISATION 2025-06-26 14:00:41 RU000A10B7T7 0 897,82 RUB 0 MCX False
86 DIVIDEND 2025-06-26 14:10:53 RU000A10B7T7 0 407,54 RUB 0 MCX False
87 BUY 2025-06-26 14:22:14 RU000A10B7T7 909,835 2 RUB 5,46 MCX 0,6 False
88 BUY 2025-06-26 14:22:34 RU000A10B7T7 909,92 1 RUB 2,73 MCX 0,6 False
89 CASH_IN 2025-07-04 15:45:31 RUB 1 2900 RUB 0 False Пополнение счета
90 CASH_IN 2025-07-04 15:45:52 RUB 1 2900 RUB 0 False Пополнение счета
91 CASH_IN 2025-07-04 23:49:07 RUB 1 2610 RUB 0 False Пополнение счета
92 CASH_IN 2025-07-04 23:49:17 RUB 1 2610 RUB 0 False Пополнение счета
93 DIVIDEND 2025-07-08 12:33:19 TPAY 0 56,4 RUB 0 MCX False
94 DIVIDEND 2025-07-08 13:58:58 TPAY 0 20,15 RUB 0 MCX False
95 BUY 2025-07-08 14:24:10 TMOS 6,2 403 RUB 0 MCX False
96 BUY 2025-07-08 14:24:21 TMOS 6,2 403 RUB 0 MCX False
97 BUY 2025-07-08 14:24:50 TBRU 6,96 286 RUB 0 MCX False
98 BUY 2025-07-08 14:24:58 TBRU 6,96 286 RUB 0 MCX False
99 BUY 2025-07-08 14:25:57 TGLD 10,37 96 RUB 0 MCX False
100 BUY 2025-07-08 14:26:05 TGLD 10,37 96 RUB 0 MCX False
101 BUY 2025-07-08 14:26:34 TPAY 98,56 8 RUB 0 MCX False
102 BUY 2025-07-08 14:26:40 TPAY 98,56 4 RUB 0 MCX False
103 CASH_IN 2025-07-11 21:28:53 RUB 1 5300 RUB 0 False Пополнение счета
104 CASH_IN 2025-07-11 21:29:03 RUB 1 5300 RUB 0 False Пополнение счета
105 BUY 2025-07-11 21:31:35 VTBR 72,84 13 RUB 2,84 MCX False
106 BUY 2025-07-11 21:31:44 VTBR 72,84 13 RUB 2,84 MCX False
107 BUY 2025-07-11 21:32:50 T 3129,4 1 RUB 0 MCX False
108 BUY 2025-07-11 21:32:58 T 3129,3 1 RUB 0 MCX False
109 DIVIDEND 2025-07-15 14:09:51 ASTR 0 6,29 RUB 1 MCX False
110 CASH_IN 2025-07-15 17:22:31 RUB 1 1000 RUB 0 False Пополнение счета
111 CASH_IN 2025-07-15 17:23:34 RUB 1 1000 RUB 0 False Пополнение счета
112 DIVIDEND 2025-07-21 07:36:14 T 0 33 RUB 4 MCX False
113 DIVIDEND 2025-07-21 08:13:36 T 0 33 RUB 4 MCX False
114 DIVIDEND 2025-07-28 20:55:02 RU000A10B7T7 0 161,19 RUB 0 MCX False
115 DIVIDEND 2025-07-28 20:59:19 RU000A10B7T7 0 17,91 RUB 0 MCX False
116 AMORTISATION 2025-07-28 21:02:33 RU000A10B7T7 0 592,11 RUB 0 MCX False
117 AMORTISATION 2025-07-28 21:06:43 RU000A10B7T7 0 65,79 RUB 0 MCX False
118 DIVIDEND 2025-08-01 11:52:28 EUTR 0 141,9 RUB 19 MCX False
119 DIVIDEND 2025-08-01 13:35:14 EUTR 0 30 RUB 4 MCX False
120 DIVIDEND 2025-08-04 14:42:34 SBERP 0 348,4 RUB 45 MCX False
121 DIVIDEND 2025-08-04 19:55:40 ROSN 0 73,4 RUB 10 MCX False
122 DIVIDEND 2025-08-04 20:07:25 ROSN 0 146,8 RUB 19 MCX False
123 DIVIDEND 2025-08-08 15:01:23 TPAY 0 57,07 RUB 0 MCX False
124 DIVIDEND 2025-08-08 15:45:28 TPAY 0 23,43 RUB 0 MCX False
125 SELL 2025-08-08 17:15:27 PIKK 638,7 10 RUB 19,16 MCX False
126 SELL 2025-08-08 17:16:06 IVAT 168,3 6 RUB 3,03 MCX False
127 SELL 2025-08-08 17:16:16 IVAT 168,25 2 RUB 1,01 MCX False
128 CASH_OUT 2025-08-08 17:18:02 RUB 1 9900 RUB 0 False Вывод со счета
129 TAX 2025-08-12 06:03:36 0 0 RUB 234 False Корректировка налога
130 SELL 2025-08-20 21:40:50 VTBR 77,91 13 RUB 3,04 MCX False
131 SELL 2025-08-20 21:41:08 EUTR 126,4 10 RUB 3,79 MCX False
132 SELL 2025-08-20 21:41:19 LENT 1785,5 1 RUB 5,36 MCX False
133 SELL 2025-08-20 21:41:56 IVAT 169,4 6 RUB 3,05 MCX False
134 SELL 2025-08-20 21:43:13 LQDT 1,7755 356 RUB 1,9 MCX False
135 SELL 2025-08-20 21:51:17 WUSH 140,58 5 RUB 2,11 MCX False
136 BUY 2025-08-21 10:50:16 TGLD 10,69 93 RUB 0 MCX False
137 BUY 2025-08-21 10:50:16 TMOS 6,76 369 RUB 0 MCX False
138 SELL 2025-08-21 11:17:38 TPAY 101,62 40 RUB 0 MCX False
139 BUY 2025-08-21 11:22:38 TMOS 6,75 381 RUB 0 MCX False
140 BUY 2025-08-21 11:22:39 TGLD 10,69 96 RUB 0 MCX False
141 BUY 2025-08-21 12:15:01 TMOS 6,75 251 RUB 0 MCX False
142 BUY 2025-08-21 12:15:01 TPAY 101,61 16 RUB 0 MCX False
143 BUY 2025-08-21 12:15:02 TGLD 10,69 63 RUB 0 MCX False
144 BUY 2025-08-21 12:26:45 SU26212RMFS9 887,83 1 RUB 2,66 MCX 5,79 False
145 BUY 2025-08-21 12:26:45 TPAY 101,6 7 RUB 0 MCX False
146 BUY 2025-08-21 12:26:46 TGLD 10,69 27 RUB 0 MCX False
147 BUY 2025-08-21 12:26:46 TMOS 6,76 107 RUB 0 MCX False
148 SELL 2025-08-21 12:43:14 ASTR 416,75 2 RUB 2,5 MCX False
149 SELL 2025-08-21 12:46:04 DELI 148,3 68 RUB 30,25 MCX False
150 SELL 2025-08-21 12:46:47 DELI 148,255 10 RUB 4,44 MCX False
151 BUY 2025-08-21 12:55:10 SU26212RMFS9 887,35 4 RUB 10,65 MCX 5,79 False
152 BUY 2025-08-21 12:55:11 TMOS 6,77 394 RUB 0 MCX False
153 BUY 2025-08-21 12:55:11 TPAY 101,58 26 RUB 0 MCX False
154 BUY 2025-08-21 12:55:12 TGLD 10,71 99 RUB 0 MCX False
155 BUY 2025-08-22 13:40:55 RNFT 124,5 50 RUB 18,68 MCX False
156 SELL 2025-08-22 13:45:06 TATN 675,1 10 RUB 20,25 MCX False
157 FEE 2025-08-23 00:30:53 0 0 RUB 45 False
158 FEE 2025-08-24 00:32:22 0 0 RUB 45 False
159 FEE 2025-08-25 00:31:52 0 0 RUB 45 False
160 FEE 2025-08-26 00:32:25 0 0 RUB 45 False
161 AMORTISATION 2025-08-26 13:06:06 RU000A10B7T7 0 664,47 RUB 0 MCX False
162 AMORTISATION 2025-08-26 13:07:38 RU000A10B7T7 0 73,83 RUB 0 MCX False
163 DIVIDEND 2025-08-26 13:14:31 RU000A10B7T7 0 153,99 RUB 0 MCX False
164 DIVIDEND 2025-08-26 13:14:58 RU000A10B7T7 0 17,11 RUB 0 MCX False
165 FEE 2025-08-27 00:36:17 0 0 RUB 45 False
166 FEE 2025-08-28 00:34:11 0 0 RUB 45 False
167 FEE 2025-08-29 00:33:46 0 0 RUB 45 False
168 DIVIDEND 2025-08-29 13:54:42 RU000A107AM4 0 165,15 RUB 0 MCX False
169 BUY 2025-08-29 17:24:07 TATN 650,5 10 RUB 19,52 MCX False
170 SELL 2025-08-29 17:24:25 RNFT 121,2 50 RUB 18,18 MCX False
171 SELL 2025-09-01 17:59:16 VTBR 74,54 13 RUB 2,91 MCX False
172 SELL 2025-09-01 17:59:29 NVTK 1239,6 5 RUB 18,59 MCX False
173 SELL 2025-09-01 17:59:39 ROSN 461,32 5 RUB 6,92 MCX False
174 SELL 2025-09-01 18:02:17 IVAT 159,7 2 RUB 0,96 MCX False
175 SELL 2025-09-01 18:02:30 CNRU 607,4 5 RUB 9,11 MCX False
176 SELL 2025-09-01 18:02:39 T 3335,8 1 RUB 0 MCX False
177 SELL 2025-09-01 18:04:27 APTK 9,532 100 RUB 2,86 MCX False
178 SELL 2025-09-01 18:04:35 WUSH 125,45 10 RUB 3,77 MCX False
179 SELL 2025-09-01 18:04:53 RU000A10B7T7 769,384908 1 RUB 2,31 MCX 3,505092 False
180 TAX 2025-09-01 18:05:10 0 0 RUB 3 False Удержание налога
181 CASH_OUT 2025-09-01 18:05:10 RUB 1 20358,92 RUB 0 False Вывод со счета
182 TAX_RETURN 2025-09-03 07:21:59 0 0 RUB 3 False Корректировка налога
183 CASH_IN 2025-09-05 11:23:58 RUB 1 5000 RUB 0 False Пополнение счета
184 DIVIDEND 2025-09-05 17:42:16 TPAY 0 60,89 RUB 0 MCX False
185 DIVIDEND 2025-09-05 18:09:43 TPAY 0 19,82 RUB 0 MCX False
186 BUY 2025-09-08 21:21:46 SU26212RMFS9 884,4 2 RUB 5,31 MCX 9,27 False
187 BUY 2025-09-08 21:21:47 TMOS 6,76 179 RUB 0 MCX False
188 BUY 2025-09-08 21:21:47 TPAY 100,84 12 RUB 0 MCX False
189 BUY 2025-09-08 21:22:16 SU26212RMFS9 884,4 1 RUB 2,65 MCX 9,27 False
190 BUY 2025-09-08 21:22:17 TMOS 6,76 107 RUB 0 MCX False
191 BUY 2025-09-08 21:22:17 TPAY 100,84 7 RUB 0 MCX False
192 AMORTISATION 2025-09-26 16:32:06 RU000A10B7T7 0 631,89 RUB 0 MCX False
193 DIVIDEND 2025-09-26 16:33:22 RU000A10B7T7 0 139,95 RUB 0 MCX False
194 CASH_IN 2025-10-05 13:15:58 RUB 1 5000 RUB 0 False Пополнение счета
195 BUY 2025-10-06 20:20:29 SU26212RMFS9 869,77 2 RUB 5,22 MCX 14,68 False
196 BUY 2025-10-06 20:20:29 TPAY 100,02 12 RUB 0 MCX False
197 BUY 2025-10-06 20:20:30 TMOS 6,17 196 RUB 0 MCX False
198 SELL 2025-10-07 13:23:38 LQDT 1,8159 356 RUB 1,94 MCX False
199 SELL 2025-10-07 13:25:10 TGLD 12,88 154 RUB 0 MCX False
200 SELL 2025-10-07 13:25:31 TMOS 6,21 801 RUB 0 MCX False
201 SELL 2025-10-07 13:25:58 TPAY 100,1 16 RUB 0 MCX False
202 SELL 2025-10-07 13:26:18 SBRB 16,599 77 RUB 3,83 MCX False
203 SELL 2025-10-07 13:26:39 TBRU 7,39 286 RUB 0 MCX False
204 CASH_OUT 2025-10-07 13:27:12 RUB 1 12614,5 RUB 0 False Вывод со счета
205 DIVIDEND 2025-10-08 10:27:11 T 0 35 RUB 4 MCX False
206 DIVIDEND 2025-10-08 16:58:14 TPAY 0 93,27 RUB 0 MCX False
207 DIVIDEND 2025-10-08 17:28:20 TPAY 0 22,24 RUB 0 MCX False
208 TAX 2025-10-09 04:18:26 0 0 RUB 22 False Корректировка налога
209 AMORTISATION 2025-10-27 18:30:50 RU000A10B7T7 0 570,42 RUB 0 MCX False
210 DIVIDEND 2025-10-27 19:16:05 RU000A10B7T7 0 122,4 RUB 0 MCX False
211 DIVIDEND 2025-10-29 14:52:01 TATNP 0 28,7 RUB 4 MCX False
212 CASH_IN 2025-11-05 11:58:33 RUB 1 5000 RUB 0 False Пополнение счета
213 BUY 2025-11-06 12:12:16 SU26212RMFS9 879,766667 3 RUB 7,92 MCX 20,66999966666667 False
214 BUY 2025-11-06 12:12:17 TMOS 5,94 327 RUB 0 MCX False
215 BUY 2025-11-06 12:12:17 TPAY 100,49 19 RUB 0 MCX False
216 BUY 2025-11-06 12:12:18 TGLD 13 59 RUB 0 MCX False
217 DIVIDEND 2025-11-10 19:06:16 TPAY 0 95,8 RUB 0 MCX False
218 AMORTISATION 2025-11-26 18:13:31 RU000A10B7T7 0 558,09 RUB 0 MCX False
219 DIVIDEND 2025-11-26 18:21:54 RU000A10B7T7 0 114,39 RUB 0 MCX False
220 CASH_IN 2025-11-27 11:05:13 RUB 1 5818,41 RUB 0 False Пополнение счета
221 BUY 2025-11-27 11:05:14 ASTR 289,1 20 RUB 17,35 MCX False
222 CASH_IN 2025-11-27 11:05:59 RUB 1 3431,27 RUB 0 False Пополнение счета
223 BUY 2025-11-27 11:06:00 IVAT 170,25 20 RUB 10,22 MCX False
224 DIVIDEND 2025-11-28 10:29:07 RU000A107AM4 0 165,15 RUB 0 MCX False
225 CASH_IN 2025-12-05 14:15:14 RUB 1 5000 RUB 0 False Пополнение счета
226 DIVIDEND 2025-12-08 19:27:48 TPAY 0 92,14 RUB 0 MCX False
227 DIVIDEND 2025-12-26 17:14:16 RU000A10B7T7 0 99,18 RUB 0 MCX False
228 AMORTISATION 2025-12-26 17:17:03 RU000A10B7T7 0 475,65 RUB 0 MCX False
229 DIVIDEND 2025-12-29 12:07:40 SFIN 0 902 RUB 117 MCX False
230 CASH_IN 2026-01-05 13:59:37 RUB 1 5000 RUB 0 False Пополнение счета
231 BUY 2026-01-06 10:22:03 TPAY 100,27 34 RUB 0 MCX False
232 BUY 2026-01-06 10:22:04 TMOS 6,45 346 RUB 0 MCX False
233 SELL 2026-01-08 11:41:58 RU000A10B7T7 502,484763 9 RUB 13,57 MCX 4,639681444444444 False
234 SELL 2026-01-08 11:41:59 RU000A107AM4 991,7 5 RUB 14,88 MCX 15,61 False
235 SELL 2026-01-08 11:41:59 SFIN 829,2 1 RUB 2,49 MCX False
236 SELL 2026-01-08 11:41:59 T 3255 1 RUB 0 MCX False
237 SELL 2026-01-08 11:42:00 SBERP 297,89 10 RUB 8,94 MCX False
238 SELL 2026-01-08 11:42:00 SU26212RMFS9 890,520769 13 RUB 34,73 MCX 32,83000023076923 False
239 SELL 2026-01-08 11:42:00 TATNP 535,4 2 RUB 3,21 MCX False
240 SELL 2026-01-08 11:42:01 ROSN 400,45 10 RUB 12,01 MCX False
241 BUY 2026-01-08 11:42:01 TMOS 6,37 2114 RUB 0 MCX False
242 BUY 2026-01-08 11:42:02 SBRB 17,683 173 RUB 9,18 MCX False
243 BUY 2026-01-08 11:42:02 TBRU 7,81 1986 RUB 0 MCX False
244 BUY 2026-01-08 11:42:02 TPAY 100,39 43 RUB 0 MCX False
245 BUY 2026-01-12 18:40:57 TDIV 10,269799 696 RUB 0 MCX False
246 SELL 2026-01-12 18:40:57 TMOS 6,380144 695 RUB 0 MCX False
247 SELL 2026-01-12 18:46:05 TDIV 10,240573 262 RUB 0 MCX False
248 DIVIDEND 2026-01-14 11:09:42 T 0 36 RUB 5 MCX False
249 DIVIDEND 2026-01-20 15:55:25 TPAY 0 136,92 RUB 0 MCX False
250 CASH_IN 2026-02-02 11:12:04 RUB 1 5000 RUB 0 False Пополнение счета
251 CASH_IN 2026-02-02 11:32:35 RUB 1 5566 RUB 0 False Пополнение счета
252 BUY 2026-02-02 11:39:30 TBRU 7,8 302 RUB 0 MCX False
253 BUY 2026-02-02 11:39:30 TOFZ 13,57 767 RUB 0 MCX False
254 BUY 2026-02-02 11:39:30 TPAY 100,08 24 RUB 0 MCX False
255 SELL 2026-02-02 11:40:19 TOFZ 13,56 397 RUB 0 MCX False
256 CASH_IN 2026-02-05 14:25:59 RUB 1 5000 RUB 0 False Пополнение счета
257 BUY 2026-02-09 11:24:18 TDIV 10,42 28 RUB 0 MCX False
258 BUY 2026-02-09 11:24:18 TMOS 6,43 279 RUB 0 MCX False
259 BUY 2026-02-09 11:24:18 TOFZ 13,53 22 RUB 0 MCX False
260 BUY 2026-02-09 11:24:19 SBRB 17,781 16 RUB 0,11 MCX False
261 BUY 2026-02-09 11:24:19 TBRU 7,8 153 RUB 0 MCX False
262 BUY 2026-02-09 11:24:19 TGLD 15,47 38 RUB 0 MCX False
263 BUY 2026-02-09 11:24:19 TPAY 100,19 11 RUB 0 MCX False
264 DIVIDEND 2026-02-09 16:36:01 TPAY 0 201,78 RUB 0 MCX False
265 BUY 2026-02-16 11:03:47 TDIV 10,69 1 RUB 0 MCX False
266 BUY 2026-02-16 11:03:47 TMOS 6,6 12 RUB 0 MCX False
267 BUY 2026-02-16 11:03:48 TBRU 7,9 6 RUB 0 MCX False
268 BUY 2026-02-16 11:03:48 TGLD 15,25 1 RUB 0 MCX False
269 SELL 2026-02-18 11:05:26 SBRB 17,917226 266 RUB 1,91 MCX False
270 BUY 2026-02-18 11:05:26 TMOS 6,56 6 RUB 0 MCX False
271 BUY 2026-02-18 11:05:26 TOFZ 13,82 380 RUB 0 MCX False
272 BUY 2026-02-18 11:05:27 TBRU 7,91 3 RUB 0 MCX False
273 SELL 2026-02-21 10:24:30 IVAT 159,55 20 RUB 1,28 MCX False
274 SELL 2026-02-21 10:24:40 ASTR 259,9 20 RUB 2,08 MCX False
275 TAX 2026-02-21 10:24:58 0 0 RUB 32 False Удержание налога по ставке 13%
276 CASH_OUT 2026-02-21 10:24:58 RUB 1 8388,99 RUB 0 False Вывод со счета
277 CASH_IN 2026-02-24 00:00:00 RUB 1 0,0024 RUB 0 False Эта сделка добавлена с целью корректировки баланса по этой валюте, т.к. баланс по данным брокера (16,14) не соответствует балансу рассчитанному по сделкам (16,14).
278 CASH_IN 2026-02-24 00:00:00 RUB 1 2000 RUB 0 False
279 BUY 2026-02-24 11:31:18 LQDT 1,9291 1036 RUB 0 MCX False
280 TAX_RETURN 2026-02-26 02:13:39 0 0 RUB 32 False Корректировка налога по ставке 13%
281 CASH_OUT 2026-03-01 18:17:42 RUB 1 32 RUB 0 False Вывод со счета
282 SELL 2026-03-02 11:31:12 TBRU 7,94 2051 RUB 0 MCX False
283 SELL 2026-03-02 11:31:12 TDIV 10,869698 463 RUB 0 MCX False
284 SELL 2026-03-02 11:31:12 TPAY 100,13 178 RUB 0 MCX False
285 SELL 2026-03-02 11:31:13 TGLD 16,69 303 RUB 0 MCX False
286 BUY 2026-03-02 11:31:13 TMOS 6,73 6144 RUB 0 MCX False
287 BUY 2026-03-02 11:31:14 TMOS 6,73 290 RUB 0 MCX False
288 BUY 2026-03-02 11:31:14 TOFZ 13,8 410 RUB 0 MCX False
289 BUY 2026-03-02 11:31:15 TBRU 7,95 17 RUB 0 MCX False
290 CASH_IN 2026-03-05 14:48:54 RUB 1 5000 RUB 0 False Пополнение счета
291 BUY 2026-03-09 11:01:40 TMOS 6,85 283 RUB 0 MCX False
292 BUY 2026-03-09 11:01:40 TOFZ 13,87 29 RUB 0 MCX False
293 BUY 2026-03-09 11:01:41 TBRU 7,97 17 RUB 0 MCX False
294 BUY 2026-03-09 11:01:41 TGLD 15,9 8 RUB 0 MCX False
295 DIVIDEND 2026-03-10 16:55:28 TPAY 0 199,98 RUB 0 MCX False
296 BUY 2026-03-16 11:35:58 TMOS 6,81 32 RUB 0 MCX False
297 BUY 2026-03-16 11:35:59 TBRU 8,07 1 RUB 0 MCX False
298 BUY 2026-03-16 11:35:59 TOFZ 14,06 3 RUB 0 MCX False
299 BUY 2026-03-23 11:11:19 TMOS 6,76 7 RUB 0 MCX False
300 BUY 2026-03-30 11:53:07 TMOS 6,56 7 RUB 0 MCX False
301 CASH_IN 2026-04-01 14:43:30 RUB 1 1100 RUB 0 False Пополнение счета
302 BUY 2026-04-01 14:47:55 RU000A10BF48 1008 1 RUB 3,02 MCX 7,46 False
303 CASH_IN 2026-04-01 20:05:46 RUB 1 100000 RUB 0 False Пополнение счета
304 BUY 2026-04-01 20:08:22 SU26207RMFS9 964,82 8 RUB 23,16 MCX 12,73 False
305 BUY 2026-04-01 20:08:40 SU26207RMFS9 964,81 8 RUB 23,16 MCX 12,73 False
306 BUY 2026-04-01 20:09:02 SU26207RMFS9 964,84 14 RUB 40,52 MCX 12,73 False
307 BUY 2026-04-01 20:09:33 SU29016RMFS1 1003,88 10 RUB 30,11 MCX 3,24 False
308 BUY 2026-04-01 20:09:47 SU29007RMFS0 1020 10 RUB 30,6 MCX 13,89 False
309 BUY 2026-04-01 20:18:20 RU000A10AT35 1029,1 5 RUB 15,44 MCX 6,62 False
310 BUY 2026-04-01 20:18:33 RU000A10BF48 1008 4 RUB 12,1 MCX 7,46 False
311 BUY 2026-04-01 20:21:25 RU000A10CQ85 999,2 5 RUB 14,99 MCX 8,44 False
312 BUY 2026-04-01 20:22:09 RU000A10B0U0 1070,5 5 RUB 16,06 MCX 1,26 False
313 BUY 2026-04-01 20:23:59 RU000A10B214 1042,4 5 RUB 15,64 MCX 13,36 False
314 CUSTOM_HOLDING_PRICE 2026-04-03 00:00:00 SIBN6P4 11353,38392 0 RUB 0 CUSTOM_HOLDING 0
315 BUY 2026-04-03 16:38:22 SIBN6P4 11353,38392 2 RUB 68,12 CUSTOM_HOLDING 25,35108 False
316 CASH_IN 2026-04-05 14:36:58 RUB 1 5000 RUB 0 False Пополнение счета
317 BUY 2026-04-06 11:24:08 TMOS 6,49 558 RUB 0 MCX False
318 BUY 2026-04-06 11:24:09 TMOS 6,49 543 RUB 0 MCX False
319 BUY 2026-04-06 11:24:10 TPAY 100,92 1 RUB 0 MCX False
320 SELL 2026-04-06 11:51:39 TMOS 6,48 539 RUB 0 MCX False
321 DIVIDEND 2026-04-07 15:55:02 RU000A10B214 0 77,05 RUB 0 MCX False
322 CASH_IN 2026-04-08 00:00:00 RUB 1 3000 RUB 0 False
323 BUY 2026-04-08 15:24:35 LQDT 1,9629 1529 RUB 0 MCX False
324 DIVIDEND 2026-04-09 13:13:04 TPAY 0 33,01 RUB 0 MCX False
325 CASH_IN 2026-04-11 21:18:45 RUB 1 5000 RUB 0 False Пополнение счета
326 BUY 2026-04-12 10:34:35 SU26207RMFS9 966,13 5 RUB 14,49 MCX 15,41 False
327 CASH_IN 2026-04-12 10:46:14 RUB 1 10000 RUB 0 False Пополнение счета
328 BUY 2026-04-12 10:46:24 SU29007RMFS0 1020,5 5 RUB 15,31 MCX 19,63 False
329 BUY 2026-04-12 10:46:32 SU29016RMFS1 1001,35 5 RUB 15,02 MCX 8,13 False
330 BUY 2026-04-13 12:54:35 TBRU 8,16 7 RUB 0 MCX False
331 BUY 2026-04-13 12:54:35 TGLD 14,24 4 RUB 0 MCX False
332 BUY 2026-04-13 12:54:35 TMOS 6,45 136 RUB 0 MCX False
333 BUY 2026-04-13 12:54:35 TOFZ 14,09 13 RUB 0 MCX False
334 DIVIDEND 2026-04-13 13:44:05 RU000A10CQ85 0 57,55 RUB 0 MCX False
335 CASH_IN 2026-04-15 22:54:40 RUB 1 1200 RUB 0 False Пополнение счета
336 SPLIT 2026-04-16 03:00:00 T 10 0 RUB 0 MCX 0
337 SPLIT 2026-04-16 03:00:00 T 10 0 RUB 0 MCX 0
338 CASH_IN 2026-04-16 14:08:35 RUB 1 1100 RUB 0 False Пополнение счета
339 CASH_IN 2026-04-16 14:08:36 RUB 1 1100 RUB 0 False Пополнение счета
340 CASH_IN 2026-04-17 00:00:00 RUB 1 1100 RUB 0 False
341 CASH_IN 2026-04-17 00:00:00 RUB 1 1110 RUB 0 False ЗАЧИСЛЕНИЕ Д/С
342 CASH_OUT 2026-04-17 00:00:00 RUB 1 1017,83999543 RUB 0 False Эта сделка добавлена с целью корректировки баланса по этой валюте, т.к. баланс по данным брокера (357,15) не соответствует балансу рассчитанному по сделкам (1374,99).
343 BUY 2026-04-17 00:00:00 STME 4,285 262 RUB 0,22 MCX False
344 DIVIDEND 2026-04-17 13:25:18 RU000A10BF48 0 69,3 RUB 0 MCX False
345 BUY 2026-04-17 15:04:10 LQDT 1,9714 558 RUB 0 MCX False
346 CASH_IN 2026-04-20 00:00:00 RUB 1 3458,88 RUB 0 False ПЕРЕВОД Д/С С ДОГОВОРА 8184V30, ТП ОСНОВНОЙ РЫНОК НА ДОГОВОР S930W42, ТП ОСНОВНОЙ РЫНОК
347 BUY 2026-04-20 12:23:33 TMOS 6,45629 124 RUB 0 MCX False
348 BUY 2026-04-20 12:23:34 TBRU 8,21 6 RUB 0 MCX False
349 BUY 2026-04-20 12:23:34 TGLD 14,35 4 RUB 0 MCX False
350 BUY 2026-04-20 12:23:34 TOFZ 14,24 12 RUB 0 MCX False
351 BUY 2026-04-20 12:33:50 RU000A10BF48 1008,2 1 RUB 3,02 MCX 2,29 False
352 BUY 2026-04-20 12:34:00 RU000A10B214 1046,6 1 RUB 3,14 MCX 7,71 False
353 BUY 2026-04-20 12:34:08 RU000A10CQ85 999,9 1 RUB 3 MCX 4,22 False
354 BUY 2026-04-20 12:34:16 RU000A10AT35 1026,6 1 RUB 3,08 MCX 1,42 False
355 DIVIDEND 2026-04-20 17:33:41 RU000A10AT35 0 70,9 RUB 0 MCX False
356 BUY 2026-04-22 00:00:00 SBRB 18,6130117 342 RUB 1,27 MCX False
357 SELL 2026-04-22 00:00:00 STME 4,24500713 701 RUB 0,6 MCX False
358 SELL 2026-04-22 15:04:08 TPAY 101,28 34 RUB 0 MCX False
359 BUY 2026-04-22 15:04:09 TBRU 8,24 2115 RUB 0 MCX False
360 SELL 2026-04-22 15:04:09 TOFZ 14,28 1239 RUB 0 MCX False
361 BUY 2026-04-22 15:04:10 TGLD 14,17 320 RUB 0 MCX False
362 BUY 2026-04-22 15:04:10 TMOS 6,51 3 RUB 0 MCX False
363 SELL 2026-04-22 15:22:42 LQDT 1,9735 3123 RUB 0 MCX False
364 BUY 2026-04-22 15:23:14 EQMX 143,2 43 RUB 1,24 MCX False
365 CASH_IN 2026-04-28 00:00:00 RUB 1 4000 RUB 0 False
366 CASH_IN 2026-04-28 00:00:00 RUB 1 4000,81 RUB 0 False ЗАЧИСЛЕНИЕ Д/С
367 BUY 2026-04-28 00:00:00 SBRB 18,68601852 216 RUB 0,8 MCX False
368 BUY 2026-04-28 11:10:13 EQMX 141,6 28 RUB 0,8 MCX False
369 DIVIDEND 2026-04-28 16:28:09 SIBN6P4 0 131,44 RUB 0 CUSTOM_HOLDING False
370 CASH_IN 2026-05-01 00:16:15 RUB 1 760 RUB 0 False Пополнение счета
371 DIVIDEND 2026-05-04 13:13:02 RU000A10B0U0 0 94,5 RUB 0 MCX False
372 CASH_IN 2026-05-05 13:48:20 RUB 1 5000 RUB 0 False Пополнение счета
373 DIVIDEND 2026-05-07 11:34:21 RU000A10B214 0 92,46 RUB 0 MCX False
374 CASH_IN 2026-05-10 10:27:31 RUB 1 2000 RUB 0 False Пополнение счета
375 BUY 2026-05-11 13:45:38 TMOS 6,36 485 RUB 0 MCX False
376 BUY 2026-05-11 13:45:39 TGLD 13,6 24 RUB 0 MCX False
377 BUY 2026-05-11 13:45:39 TMOS 6,36 464 RUB 0 MCX False
378 CASH_IN 2026-05-12 16:23:54 RUB 1 9000 RUB 0 False Пополнение счета
379 BUY 2026-05-12 16:26:57 RU000A10B0U0 1066,7 5 RUB 16 MCX 8,19 False
380 BUY 2026-05-12 16:27:09 RU000A10AT35 1031,5 4 RUB 12,38 MCX 11,82 False
381 BUY 2026-05-12 16:27:44 RU000A10BF48 1008 2 RUB 6,05 MCX 12,19 False
382 DIVIDEND 2026-05-13 12:22:10 RU000A10CQ85 0 69,06 RUB 0 MCX False
383 SELL 2026-05-18 14:13:35 TMOS 6,3 488 RUB 0 MCX False
384 DIVIDEND 2026-05-18 17:46:19 RU000A10AT35 0 141,8 RUB 0 MCX False
385 DIVIDEND 2026-05-19 12:28:16 RU000A10BF48 0 108,16 RUB 0 MCX False
386 BUY 2026-05-25 14:33:45 TMOS 6,28 98 RUB 0 MCX False
387 BUY 2026-05-25 14:33:46 TBRU 8,32 21 RUB 0 MCX False
388 BUY 2026-05-25 14:33:46 TGLD 12,81 5 RUB 0 MCX False
389 DIVIDEND 2026-05-26 18:44:26 SIBN6P4 0 124,94 RUB 0 CUSTOM_HOLDING False
390 CASH_IN 2026-05-28 00:00:00 RUB 1 4000 RUB 0 False
391 CASH_IN 2026-05-28 00:00:00 RUB 1 4000 RUB 0 False ЗАЧИСЛЕНИЕ Д/С
392 BUY 2026-05-28 00:00:00 SBRB 18,89099057 212 RUB 0,8 MCX False
393 BUY 2026-05-28 11:06:55 EQMX 136,3 29 RUB 0,79 MCX False
394 BUY 2026-06-01 16:08:51 TBRU 8,33 1 RUB 0 MCX False
395 BUY 2026-06-01 16:08:51 TMOS 6,15 5 RUB 0 MCX False
396 DIVIDEND 2026-06-02 12:30:21 RU000A10B0U0 0 189 RUB 0 MCX False
397 CASH_IN 2026-06-05 12:10:51 RUB 1 5000 RUB 0 False Пополнение счета
398 DIVIDEND 2026-06-08 12:41:38 RU000A10B214 0 92,46 RUB 0 MCX False
399 BUY 2026-06-08 14:44:52 TMOS 6,1 588 RUB 0 MCX False
400 BUY 2026-06-08 14:44:52 TMOS 6,1 576 RUB 0 MCX False
401 SELL 2026-06-08 14:47:22 TMOS 6,09 569 RUB 0 MCX False
402 BUY 2026-06-08 15:13:02 TMOS 6,09 151 RUB 0 MCX False
403 BUY 2026-06-08 15:13:03 TBRU 8,36 31 RUB 0 MCX False
404 BUY 2026-06-08 15:13:03 TGLD 12,48 8 RUB 0 MCX False
405 DIVIDEND 2026-06-10 13:22:42 RU000A10CQ85 0 69,06 RUB 0 MCX False
406 BUY 2026-06-15 16:28:53 TMOS 6,06 3 RUB 0 MCX False
407 CASH_IN 2026-06-15 19:47:02 RUB 1 10000 RUB 0 False Пополнение счета
408 BUY 2026-06-15 20:22:47 RU000A10B214 1039,9 4 RUB 12,48 MCX 5,65 False
409 BUY 2026-06-15 20:22:56 RU000A10BF48 1008 2 RUB 6,05 MCX 0,44 False
410 BUY 2026-06-15 20:23:06 RU000A10CQ85 1004,2 4 RUB 12,05 MCX 2,68 False
411 DIVIDEND 2026-06-16 15:47:20 RU000A10BF48 0 106,56 RUB 0 MCX False
412 DIVIDEND 2026-06-18 10:10:16 RU000A10AT35 0 141,8 RUB 0 MCX False
413 BUY 2026-06-22 16:08:34 TBRU 8,28 3 RUB 0 MCX False
414 BUY 2026-06-22 16:08:34 TMOS 5,63 17 RUB 0 MCX False
415 DIVIDEND 2026-06-24 16:34:47 SU29016RMFS1 0 538,05 RUB 0 MCX False
416 DIVIDEND 2026-06-25 19:11:14 SIBN6P4 0 131,06 RUB 0 CUSTOM_HOLDING False
417 BUY 2026-06-26 23:28:57 TMOS 5,51 417 RUB 0 MCX False
418 CASH_IN 2026-06-28 00:00:00 RUB 1 4000 RUB 0 False
419 CASH_IN 2026-06-28 00:00:00 RUB 1 4000 RUB 0 False ЗАЧИСЛЕНИЕ Д/С
420 BUY 2026-06-28 00:00:00 SBRB 18,76201878 213 RUB 0,8 MCX False
421 BUY 2026-06-28 14:13:46 EQMX 121,6 33 RUB 0 MCX False
422 DIVIDEND 2026-06-30 12:00:37 RU000A10B0U0 0 189 RUB 0 MCX False
423 CASH_IN 2026-07-01 12:25:32 RUB 1 1000 RUB 0 False Пополнение счета
424 CASH_IN 2026-07-01 12:25:56 RUB 1 1000 RUB 0 False Пополнение счета
425 BUY 2026-07-01 12:39:46 TGLD 12,17 68 RUB 0 MCX False
426 DIVIDEND 2026-07-07 13:30:58 RU000A10B214 0 154,1 RUB 0 MCX False
427 DIVIDEND 2026-07-10 10:36:35 RU000A10CQ85 0 115,1 RUB 0 MCX False
428 BUY 2026-07-10 10:43:57 RU000A10CQ85 997,1 1 RUB 0,4 MCX 1,53 False
429 BUY 2026-07-10 10:44:04 RU000A10CQ85 997,2 2 RUB 0,8 MCX 1,53 False
430 CASH_IN 2026-07-13 11:03:09 RUB 1 10000 RUB 0 False Пополнение счета
431 CASH_IN 2026-07-13 11:03:15 RUB 1 10000 RUB 0 False Пополнение счета
432 CASH_IN 2026-07-13 11:37:10 RUB 1 5000 RUB 0 False Пополнение счета
433 BUY 2026-07-13 12:32:18 TBRU 8,21 217 RUB 0 MCX False
434 BUY 2026-07-13 12:32:18 TMOS 5,29 1523 RUB 0 MCX False
435 BUY 2026-07-13 12:32:19 TGLD 12,23 58 RUB 0 MCX False
436 BUY 2026-07-13 12:33:16 RU000A10B0U0 1044,2 3 RUB 1,25 MCX 9,45 False
437 BUY 2026-07-14 07:18:33 RU000A10B0U0 1045 2 RUB 0,84 MCX 10,08 False
438 BUY 2026-07-14 07:18:48 RU000A10CQ85 998,3 2 RUB 0,8 MCX 2,3 False
439 BUY 2026-07-14 07:18:57 RU000A10AT35 1026,2 5 RUB 2,05 MCX 13,23 False
440 BUY 2026-07-14 07:19:14 SU29016RMFS1 1000,96 2 RUB 0,8 MCX 8,13 False
441 DIVIDEND 2026-07-16 12:32:09 RU000A10BF48 0 131,8 RUB 0 MCX False
442 DIVIDEND 2026-07-17 17:42:53 RU000A10AT35 0 212,7 RUB 0 MCX False
443 BUY 2026-07-19 15:39:27 TMOS 4,77 130 RUB 0 MCX False
444 SELL 2026-07-20 13:22:33 TBRU 8,06 290 RUB 0 MCX False
445 CASH_IN 2026-07-23 00:00:00 RUB 1 10000 RUB 0 False ЗАЧИСЛЕНИЕ Д/С
446 BUY 2026-07-23 00:00:00 SBER 263,68 17 RUB 13,45 MCX False
447 BUY 2026-07-23 00:00:00 SBER 263,68 3 RUB 2,37 MCX False
448 BUY 2026-07-24 00:00:00 MOEX 146,28 10 RUB 4,39 MCX False
449 BUY 2026-07-24 00:00:00 MOEX 146,28 20 RUB 8,78 MCX False
450 DIVIDEND 2026-07-28 09:53:12 SIBN6P4 0 138,06 RUB 0 CUSTOM_HOLDING False
451 DIVIDEND 2026-07-30 11:50:58 RU000A10B0U0 0 283,5 RUB 0 MCX False
452 BUY 2026-07-30 12:13:11 SU26207RMFS9 978,99 1 RUB 2,94 MCX 39,52 False
453 DIVIDEND 2026-08-05 13:31:58 RU000A10B214 0 154,1 RUB 0 MCX False
454 DIVIDEND 2026-08-05 18:29:11 SU26207RMFS9 0 1463,04 RUB 0 MCX False
455 BUY 2026-08-05 22:37:13 SU26207RMFS9 978,88 1 RUB 2,94 MCX 0,22 False
456 DIVIDEND 2026-08-11 13:40:31 RU000A10CQ85 0 172,65 RUB 0 MCX False
457 BUY 2026-08-13 00:00:00 LKOH 4534,5 1 RUB 14,96 MCX False
458 CASH_IN 2026-08-13 00:00:00 RUB 1 4000 RUB 0 False
459 CASH_IN 2026-08-13 00:00:00 RUB 1 10551,18 RUB 0 False ПЕРЕВОД Д/С С ДОГОВОРА 8184V30, ТП ОСНОВНОЙ РЫНОК НА ДОГОВОР S930W42, ТП ОСНОВНОЙ РЫНОК
460 BUY 2026-08-13 00:00:00 TATN 548,3 9 RUB 16,29 MCX False
461 CASH_IN 2026-08-13 11:05:50 RUB 1 14000 RUB 0 False Пополнение счета
462 CASH_IN 2026-08-13 13:06:52 RUB 1 10000 RUB 0 False Пополнение счета
463 BUY 2026-08-13 15:56:15 EQMX 122,9 33 RUB 0,81 MCX False
464 BUY 2026-08-14 14:13:20 SU29016RMFS1 1000,8 5 RUB 15,01 MCX 20,92 False
465 BUY 2026-08-14 14:13:42 SU26207RMFS9 979,08 6 RUB 17,62 MCX 2,68 False
466 DIVIDEND 2026-08-17 13:13:29 RU000A10BF48 0 130,1 RUB 0 MCX False
467 DIVIDEND 2026-08-17 16:37:48 RU000A10AT35 0 212,7 RUB 0 MCX False
468 BUY 2026-08-18 14:52:52 TBRU 8,41 427 RUB 0 MCX False
469 BUY 2026-08-18 14:52:52 TMOS 5,34 1843 RUB 0 MCX False
470 DIVIDEND 2026-08-26 09:18:46 SIBN6P4 0 147,02 RUB 0 CUSTOM_HOLDING False
471 CASH_IN 2026-08-27 11:04:19 RUB 1 450 RUB 0 False Пополнение счета
472 BUY 2026-08-27 11:04:52 TMOS 5,16 121 RUB 0 MCX False
473 CASH_IN 2026-08-28 11:45:37 RUB 1 450 RUB 0 False Пополнение счета
474 CASH_IN 2026-08-29 12:12:09 RUB 1 450 RUB 0 False Пополнение счета
475 CASH_IN 2026-08-30 12:07:12 RUB 1 450 RUB 0 False Пополнение счета
476 BUY 2026-08-31 11:00:16 TBRU 8,44 40 RUB 0 MCX False
477 BUY 2026-08-31 11:00:16 TMOS 5,33 173 RUB 0 MCX False
478 DIVIDEND 2026-08-31 12:29:47 RU000A10B0U0 0 283,5 RUB 0 MCX False
479 CASH_IN 2026-08-31 13:44:09 RUB 1 450 RUB 0 False Пополнение счета
480 BUY 2026-09-01 11:00:11 TBRU 8,46 25 RUB 0 MCX False
481 BUY 2026-09-01 11:00:11 TMOS 5,43 38 RUB 0 MCX False
482 CASH_IN 2026-09-01 12:49:47 RUB 1 450 RUB 0 False Пополнение счета
483 BUY 2026-09-02 11:00:17 TBRU 8,45 18 RUB 0 MCX False
484 BUY 2026-09-02 11:00:17 TMOS 5,4 56 RUB 0 MCX False
485 CASH_IN 2026-09-02 14:10:10 RUB 1 450 RUB 0 False Пополнение счета
486 DIVIDEND 2026-09-03 10:57:34 SU29007RMFS0 0 1307,4 RUB 0 MCX False
487 BUY 2026-09-03 11:00:18 TBRU 8,45 113 RUB 0 MCX False
488 BUY 2026-09-03 11:00:18 TMOS 5,45 425 RUB 0 MCX False
489 BUY 2026-09-03 11:20:02 SU26207RMFS9 980,48 2 RUB 5,88 MCX 6,7 False
490 CASH_IN 2026-09-03 13:52:30 RUB 1 450 RUB 0 False Пополнение счета
491 BUY 2026-09-04 11:00:21 TMOS 5,58 9 RUB 0 MCX False
492 BUY 2026-09-04 11:00:22 TBRU 8,45 50 RUB 0 MCX False
493 CASH_IN 2026-09-04 11:31:06 RUB 1 450 RUB 0 False Пополнение счета
494 DIVIDEND 2026-09-04 12:06:55 RU000A10B214 0 154,1 RUB 0 MCX False
495 CASH_IN 2026-09-05 13:46:10 RUB 1 450 RUB 0 False Пополнение счета
496 CASH_IN 2026-09-06 14:12:11 RUB 1 450 RUB 0 False Пополнение счета
497 BUY 2026-09-07 11:00:18 TBRU 8,46 66 RUB 0 MCX False
498 BUY 2026-09-07 11:00:18 TMOS 5,58 140 RUB 0 MCX False
499 BUY 2026-09-08 11:00:20 TBRU 8,46 2 RUB 0 MCX False
500 DIVIDEND 2026-09-08 13:02:22 RU000A10CQ85 0 172,65 RUB 0 MCX False
501 CASH_IN 2026-09-11 16:30:24 RUB 1 2300 RUB 0 False Пополнение счета
502 BUY 2026-09-11 18:32:59 TBRU 8,47 97 RUB 0 MCX False
503 BUY 2026-09-11 18:32:59 TGLD 14,32 41 RUB 0 MCX False
504 BUY 2026-09-11 18:32:59 TMOS 5,69 151 RUB 0 MCX False
505 CUSTOM_HOLDING_PRICE 2026-09-12 00:00:00 SIBN6P4 12228,06098 0 RUB 0 CUSTOM_HOLDING 0
506 CASH_IN 2026-09-12 11:31:49 RUB 1 450 RUB 0 False Пополнение счета
507 CUSTOM_HOLDING_PRICE 2026-09-13 00:00:00 SIBN6P4 12228,06098 0 RUB 0 CUSTOM_HOLDING 0
508 CASH_IN 2026-09-13 13:32:57 RUB 1 450 RUB 0 False Пополнение счета
509 CASH_IN 2026-09-13 14:37:16 RUB 1 10000 RUB 0 False Пополнение счета
510 CUSTOM_HOLDING_PRICE 2026-09-14 00:00:00 SIBN6P4 12320,94504 0 RUB 0 CUSTOM_HOLDING 0
511 BUY 2026-09-14 11:00:21 TBRU 8,48 60 RUB 0 MCX False
512 BUY 2026-09-14 11:00:21 TMOS 5,78 10 RUB 0 MCX False
513 BUY 2026-09-14 11:00:22 TGLD 14,15 25 RUB 0 MCX False
514 CASH_IN 2026-09-14 14:16:05 RUB 1 450 RUB 0 False Пополнение счета
515 CUSTOM_HOLDING_PRICE 2026-09-15 00:00:00 SIBN6P4 12284,594 0 RUB 0 CUSTOM_HOLDING 0
516 BUY 2026-09-15 11:00:18 TBRU 8,49 39 RUB 0 MCX False
517 BUY 2026-09-15 11:00:18 TGLD 14,2 8 RUB 0 MCX False
518 CASH_IN 2026-09-15 14:34:11 RUB 1 450 RUB 0 False Пополнение счета
519 DIVIDEND 2026-09-15 15:56:39 RU000A10BF48 0 129 RUB 0 MCX False
520 DIVIDEND 2026-09-15 16:51:49 RU000A10AT35 0 212,7 RUB 0 MCX False
521 BUY 2026-09-16 00:00:00 AFLT 32,65 30 RUB 3,22 MCX 0 RUB False
522 BUY 2026-09-16 00:00:00 AFLT 32,66 100 RUB 10,78 MCX 0 RUB False
523 BUY 2026-09-16 00:00:00 CHMF 638,4 2 RUB 4,22 MCX 0 RUB False
524 BUY 2026-09-16 00:00:00 CHMF 639,4 5 RUB 10,55 MCX 0 RUB False
525 BUY 2026-09-16 00:00:00 MSNG 1,44 1000 RUB 4,75 MCX 0 RUB False
526 BUY 2026-09-16 00:00:00 MSNG 1,44 2000 RUB 9,49 MCX 0 RUB False
527 BUY 2026-09-16 00:00:00 MTSS 189,8 2 RUB 1,26 MCX 0 RUB False
528 BUY 2026-09-16 00:00:00 MTSS 189,1 10 RUB 6,25 MCX 0 RUB False
529 BUY 2026-09-16 00:00:00 MTSS 190,25 5 RUB 3,14 MCX 0 RUB False
530 BUY 2026-09-16 00:00:00 PHOR 5309 2 RUB 35,05 MCX 0 RUB False
531 CASH_IN 2026-09-16 00:00:00 RUB 1 10000 RUB 0 False
532 SELL 2026-09-16 00:00:00 SBRB 19,06 983 RUB 3,75 MCX 0 RUB False
533 BUY 2026-09-16 11:00:22 TMOS 5,8 80 RUB 0 MCX False
534 CASH_IN 2026-09-16 11:31:47 RUB 1 450 RUB 0 False Пополнение счета
535 CUSTOM_HOLDING_SETTINGS 2026-09-17 00:00:00 SIBN6P4 0 0 RUB 0 CUSTOM_HOLDING 0 {@*@Holding@*@:{@*@Note@*@:null,@*@Currency@*@:@*@RUB@*@,@*@Description@*@:@*@\u0413\u0430\u0437\u043F\u0440\u043E\u043C \u041D\u0435\u0444\u0442\u044C 006\u0420-04@*@,@*@DividendTax@*@:null,@*@Sector@*@:@*@Other@*@},@*@Settings@*@:{@*@CustomHoldingType@*@:0,@*@IncomeType@*@:1,@*@FirstIncomeDate@*@:null,@*@GenerateIncome@*@:false,@*@IncomeAmount@*@:null,@*@IncomeReinvestmentType@*@:0,@*@IsIncomeReinvested@*@:false,@*@MaturityDate@*@:null,@*@Period@*@:null,@*@PeriodType@*@:0,@*@NextIncomeDate@*@:null}}
536 BUY 2026-09-17 11:00:21 TMOS 5,61 80 RUB 0 MCX False
537 CASH_IN 2026-09-17 11:29:23 RUB 1 450 RUB 0 False Пополнение счета
538 BUY 2026-09-17 16:23:00 RU000A10BK09 1014,4 2 RUB 6,09 MCX 8,33 False
539 BUY 2026-09-17 16:23:11 RU000A10BK09 1014,4 3 RUB 9,13 MCX 8,33 False
540 BUY 2026-09-17 16:23:18 RU000A10BK09 1014,5 1 RUB 3,04 MCX 8,33 False
541 BUY 2026-09-17 16:23:50 RU000A10BF48 1006 4 RUB 12,07 MCX 2,15 False
542 SELL 2026-09-17 16:24:21 RU000A10CQ85 1000,2 9 RUB 27 MCX 4,22 False
543 SELL 2026-09-17 16:24:26 RU000A10CQ85 1000,1 1 RUB 3 MCX 4,22 False
544 SELL 2026-09-17 16:24:30 RU000A10CQ85 1000 5 RUB 15 MCX 4,22 False
545 BUY 2026-09-17 16:24:46 RU000A10BK09 1014,4 7 RUB 21,3 MCX 8,33 False
546 BUY 2026-09-17 16:24:54 RU000A10BF48 1006 8 RUB 24,14 MCX 2,15 False
+420
View File
@@ -0,0 +1,420 @@
"""The universal event-CSV parser, checked against the real Snowball export.
The fixture is the only file in the project that covers every brokerage account at once,
so these tests double as its documentation: the exact number of rows of each type, which
of them deliberately never become ledger events, and what the format's ambiguities were
decided to mean. A change in any of those numbers is a change in the import contract, not
a test that needs relaxing.
"""
from __future__ import annotations
import collections
import dataclasses
from decimal import Decimal
from pathlib import Path
import pytest
from fintracker.models.ledger import EventKind
from fintracker.sources.reports.base import BrokerEvent, ParsedReport, fingerprint_key
from fintracker.sources.reports.csv_universal import UniversalCsvParser
FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "reports"
SNOWBALL = FIXTURES / "snowball"
HEADER = (
"Event,Date,Symbol,Price,Quantity,Currency,FeeTax,Exchange,NKD,FeeCurrency,DoNotAdjustCash,Note"
)
#: What the fixture must yield, per kind. Sums to 535 of its 545 rows: 5 CUSTOM_HOLDING_PRICE
#: and 1 CUSTOM_HOLDING_SETTINGS are not events at all, and 4 cash rows are Snowball's own
#: balance corrections.
EXPECTED_COUNTS = {
EventKind.buy: 270,
EventKind.sell: 60,
EventKind.dividend: 80,
EventKind.deposit: 96,
EventKind.withdrawal: 5,
EventKind.amortization: 9,
EventKind.commission: 7,
EventKind.tax: 4,
EventKind.tax_refund: 2,
EventKind.stock_split: 2,
}
@pytest.fixture(scope="module")
def parser() -> UniversalCsvParser:
return UniversalCsvParser()
@pytest.fixture(scope="module")
def fixture_path() -> Path:
files = sorted(SNOWBALL.glob("*.csv"))
assert files, f"нет фикстуры в {SNOWBALL}"
return files[0]
@pytest.fixture(scope="module")
def report(parser: UniversalCsvParser, fixture_path: Path) -> ParsedReport:
return parser.parse(fixture_path.read_bytes(), fixture_path.name)
def _csv(*rows: str) -> bytes:
return ("" + "\n".join((HEADER, *rows)) + "\n").encode("utf-8")
def _key(report: ParsedReport, event: BrokerEvent) -> str:
"""The dedupe key `ledger/ingest.py` will build for this event (plan §1.6 A)."""
return fingerprint_key(
report.broker,
report.account_external_id,
event.kind,
event.instrument.key() if event.instrument else "",
event.trade_date,
event.quantity,
event.price,
event.currency,
amount=event.amount,
seq=event.seq,
)
def _floats(value: object, path: str) -> list[str]:
"""Every float reachable from `value`, by path — money must be Decimal end to end."""
if isinstance(value, bool | str | bytes) or value is None:
return []
if isinstance(value, float):
return [f"{path} = {value!r}"]
if dataclasses.is_dataclass(value) and not isinstance(value, type):
return [
bad
for f in dataclasses.fields(value)
for bad in _floats(getattr(value, f.name), f"{path}.{f.name}")
]
if isinstance(value, dict):
return [bad for k, v in value.items() for bad in _floats(v, f"{path}[{k!r}]")]
if isinstance(value, list | tuple | set):
return [bad for i, v in enumerate(value) for bad in _floats(v, f"{path}[{i}]")]
return []
# --- 1. sniff -------------------------------------------------------------------------
def test_sniff_recognises_the_fixture(parser: UniversalCsvParser, fixture_path: Path) -> None:
assert parser.sniff(fixture_path.read_bytes(), fixture_path.name) is True
def test_sniff_ignores_the_filename(parser: UniversalCsvParser) -> None:
"""Recognition is by the header, so a renamed export is still recognised..."""
data = _csv('"CASH_IN","2025-02-26 09:22:54","RUB","1","5000","RUB","0","","","","False",""')
assert parser.sniff(data, "какой-то-файл.csv") is True
assert parser.sniff(data, "") is True
def test_sniff_rejects_other_report_formats(parser: UniversalCsvParser) -> None:
"""...and a file that merely has an extension we read is not ours."""
others = sorted((FIXTURES / "sber").glob("*.html")) + sorted((FIXTURES / "vtb").glob("*.xlsx"))
assert others, "нет чужих фикстур для отрицательной проверки"
for path in others:
assert parser.sniff(path.read_bytes(), path.name) is False, path.name
def test_sniff_rejects_a_csv_with_foreign_columns(parser: UniversalCsvParser) -> None:
assert parser.sniff(b"a,b,c\n1,2,3\n", "x.csv") is False
assert parser.sniff(b"", "x.csv") is False
assert parser.sniff(b"\x00\x01\x02", "x.csv") is False
def test_parse_rejects_a_file_it_cannot_read(parser: UniversalCsvParser) -> None:
from fintracker.sources.reports.base import ParseError
with pytest.raises(ParseError):
parser.parse(b"a,b,c\n1,2,3\n", "x.csv")
with pytest.raises(ParseError):
parser.parse(_csv(), "empty.csv")
# --- 2. what the fixture yields --------------------------------------------------------
def test_report_header(report: ParsedReport) -> None:
import datetime
assert report.broker == "csv"
assert report.parser_version == "1"
# the file names a portfolio, never an account — see the warning below
assert report.account_external_id == "Мой капитал"
assert report.period_from == datetime.date(2025, 2, 26)
assert report.period_to == datetime.date(2026, 9, 17)
def test_event_counts_by_kind(report: ParsedReport) -> None:
counts = collections.Counter(e.kind for e in report.events)
assert dict(counts) == EXPECTED_COUNTS
assert len(report.events) == 535
assert report.meta["row_count"] == 545
def test_instruments_are_deduplicated(report: ParsedReport) -> None:
keys = [ref.key() for ref in report.instruments]
assert len(keys) == len(set(keys)) == 49
# bonds arrive by ISIN, everything else by ticker; cash rows name no instrument
assert "ISIN:RU000A10B7T7" in keys
assert "TICKER:SBER" in keys
assert "TICKER:SU26212RMFS9" in keys, "секунда ОФЗ — не ISIN, а тикер MOEX"
assert not any(k.startswith("TICKER:RUB") for k in keys)
# --- 3. decimals ----------------------------------------------------------------------
def test_decimal_comma_is_parsed(parser: UniversalCsvParser) -> None:
data = _csv(
'"BUY","2025-03-01 10:00:00","SBER","4,48","219","RUB","6,57","MCX","0","RUB","False",""'
)
(event,) = parser.parse(data, "renamed.csv").events
assert event.price == Decimal("4.48")
assert event.quantity == Decimal("219")
assert event.fee == Decimal("6.57")
assert event.amount == Decimal("-987.69") # 4.48 * 219 + 6.57, fee capitalised
def test_no_floats_anywhere_in_the_output(report: ParsedReport) -> None:
bad = _floats(report, "report")
assert bad == []
def test_empty_numeric_cell_is_none_not_zero(parser: UniversalCsvParser) -> None:
"""An absent НКД is not a zero НКД: one is «not a bond», the other is «no interest»."""
data = _csv('"BUY","2025-03-01 10:00:00","SBER","100","1","RUB","0","MCX","","","False",""')
(event,) = parser.parse(data, "x.csv").events
assert event.accrued_interest is None
assert event.fee == Decimal(0)
def test_bond_trade_adds_accrued_interest_to_the_money(report: ParsedReport) -> None:
"""НКД is paid on top of the price by the buyer — it must be inside `amount`."""
buy = next(
e
for e in report.events
if e.kind is EventKind.buy
and e.instrument is not None
and e.instrument.isin == "RU000A107AM4"
and e.accrued_interest == Decimal("5.08")
)
assert buy.quantity == Decimal(4)
assert buy.price == Decimal("929.25")
assert buy.amount == -(Decimal(4) * Decimal("929.25") + Decimal("5.08") + Decimal("11.15"))
# --- 4. dividends and coupons ----------------------------------------------------------
def test_dividend_money_lands_in_amount_not_quantity(report: ParsedReport) -> None:
dividends = [e for e in report.events if e.kind is EventKind.dividend]
assert len(dividends) == 80
assert all(e.quantity is None for e in dividends), "Quantity у DIVIDEND — это деньги"
assert all(e.price is None for e in dividends)
assert all(e.amount > 0 for e in dividends)
assert sum(e.amount for e in dividends) == Decimal("12949.07")
def test_bond_dividends_carry_the_coupon_hint(report: ParsedReport) -> None:
"""The parser cannot know an asset class, so it hands ingest a suspicion, not a kind."""
dividends = [e for e in report.events if e.kind is EventKind.dividend]
hinted = [e for e in dividends if e.meta.get("payout_hint") == "coupon"]
assert len(hinted) == 44
assert all(
e.instrument is not None
and (
(e.instrument.isin or "").startswith("RU000A")
or (e.instrument.ticker or "").startswith("SU")
)
for e in hinted
)
# a share payout must never be hinted
assert all(
e.meta.get("payout_hint") is None
for e in dividends
if e.instrument is not None and e.instrument.ticker in {"T", "ROSN", "SBERP", "TATNP"}
)
# --- 5. splits -------------------------------------------------------------------------
def test_split_ratio_and_collapsing_dedupe_key(report: ParsedReport) -> None:
"""Two identical SPLIT rows for T are one corporate action seen on two accounts.
The format has no account column, so both rows land on the single account the user
points the import at. They therefore share a `fingerprint_key` on purpose: replaying the
ratio twice would multiply the position by 100 instead of by 10. This test pins that
behaviour — if a future format gains an account column, it is expected to break.
"""
splits = [e for e in report.events if e.kind is EventKind.stock_split]
assert len(splits) == 2
assert {e.meta["split_ratio"] for e in splits} == {Decimal(10)}
assert {e.meta["ratio"] for e in splits} == {Decimal(10)}
assert all(e.amount == Decimal(0) and e.quantity is None for e in splits)
assert all(e.instrument is not None and e.instrument.ticker == "T" for e in splits)
assert [e.seq for e in splits] == [0, 0]
assert _key(report, splits[0]) == _key(report, splits[1])
def test_every_other_event_keeps_a_distinct_dedupe_key(report: ParsedReport) -> None:
"""The split pair is the ONLY intentional collapse in the file.
In particular the pairs of equal same-day top-ups (two 1 100 ₽ one second apart, two
4 000 ₽ that differ only by their note) survive as distinct events: they are real money
into two accounts, and netting them would understate the XIRR denominator.
"""
keys = [_key(report, e) for e in report.events]
collapsed = [k for k, n in collections.Counter(keys).items() if n > 1]
assert len(collapsed) == 1
assert len(set(keys)) == 534
same_second = [e for e in report.events if e.kind is EventKind.deposit and e.seq > 0]
assert same_second, "в файле есть одинаковые пополнения одного дня — они должны выживать"
def test_seq_is_stable_across_reparsing(parser: UniversalCsvParser, fixture_path: Path) -> None:
"""A re-import of the same bytes must reproduce every key, or nothing ever upserts."""
data = fixture_path.read_bytes()
first = parser.parse(data, fixture_path.name)
second = parser.parse(data, "совсем другое имя.csv")
assert [_key(first, e) for e in first.events] == [_key(first, e) for e in second.events]
# --- 6. amortisation -------------------------------------------------------------------
def test_amortisation_is_money_on_a_bond(report: ParsedReport) -> None:
amortisations = [e for e in report.events if e.kind is EventKind.amortization]
assert len(amortisations) == 9
assert all(e.amount > 0 for e in amortisations)
assert all(e.quantity is None and e.price is None for e in amortisations)
assert {e.instrument.isin for e in amortisations if e.instrument} == {"RU000A10B7T7"}
assert all(
e.instrument is not None and e.instrument.asset_class_hint == "bond" for e in amortisations
)
assert sum(e.amount for e in amortisations) == Decimal("4530.07")
# --- 7. manual prices ------------------------------------------------------------------
def test_custom_holding_prices_go_to_meta_not_to_events(report: ParsedReport) -> None:
"""CUSTOM_HOLDING_PRICE is a `price_manual` row, not a ledger event."""
prices = report.meta["manual_prices"]
assert len(prices) == 5
assert {p["instrument_key"] for p in prices} == {"TICKER:SIBN6P4"}
assert {p["currency"] for p in prices} == {"RUB"}
assert all(isinstance(p["price"], Decimal) for p in prices)
assert max(p["price"] for p in prices) == Decimal("12320.94504")
assert all(e.meta.get("csv_event") != "CUSTOM_HOLDING_PRICE" for e in report.events)
# the instrument they price must be resolvable, so it is also listed
assert "TICKER:SIBN6P4" in {ref.key() for ref in report.instruments}
def test_the_export_has_no_price_for_the_other_unquoted_paper(report: ParsedReport) -> None:
"""NDM_TBNK-PP-FIXPRCNT-08.25 is simply absent — this file cannot price it."""
named = {(ref.ticker or "") + (ref.isin or "") for ref in report.instruments}
assert not any("NDM" in name for name in named)
assert not any("NDM" in p["instrument_key"] for p in report.meta["manual_prices"])
def test_custom_holding_settings_describe_an_instrument(report: ParsedReport) -> None:
"""Its Note is JSON with `"` rewritten to `@*@` and Cyrillic as \\uXXXX escapes."""
ref = next(r for r in report.instruments if r.ticker == "SIBN6P4")
assert ref.name == "Газпром Нефть 006Р-04"
assert ref.currency == "RUB"
assert ref.meta["custom_holding"] is True
assert ref.meta["sector"] == "Other"
assert ref.meta["settings"]["CustomHoldingType"] == 0
assert all(e.meta.get("csv_event") != "CUSTOM_HOLDING_SETTINGS" for e in report.events)
def test_broken_settings_json_is_a_warning_not_a_crash(parser: UniversalCsvParser) -> None:
data = _csv(
'"CUSTOM_HOLDING_SETTINGS","2026-09-17 00:00:00","XYZ","0","0","RUB","0",'
'"CUSTOM_HOLDING","0","","","{@*@Holding@*@:'
)
result = parser.parse(data, "x.csv")
assert result.events == []
assert any("не удалось разобрать JSON" in w for w in result.warnings)
assert [ref.key() for ref in result.instruments] == ["TICKER:XYZ"]
# --- 8. balance adjustments ------------------------------------------------------------
def test_balance_adjustments_never_become_flows(report: ParsedReport) -> None:
"""Snowball's own corrections would feed XIRR an external flow that never happened."""
adjustments = report.meta["balance_adjustments"]
assert len(adjustments) == 4
assert collections.Counter(a["kind"] for a in adjustments) == {"CASH_OUT": 3, "CASH_IN": 1}
assert all(isinstance(a["amount"], Decimal) for a in adjustments)
# detection is by the note, not by the amount: one correction is 1017,84 ₽ while a
# genuine withdrawal in the same file is 32 ₽
assert min(abs(a["amount"]) for a in adjustments) == Decimal("0.0024")
assert max(abs(a["amount"]) for a in adjustments) == Decimal("1017.83999543")
excluded = {(a["kind"], a["d"], abs(a["amount"])) for a in adjustments}
for event in report.events:
kind = "CASH_IN" if event.kind is EventKind.deposit else "CASH_OUT"
assert (kind, event.trade_date, abs(event.amount)) not in excluded
assert any("правка остатка" in w for w in report.warnings)
def test_real_cash_flows_survive(report: ParsedReport) -> None:
deposits = [e for e in report.events if e.kind is EventKind.deposit]
withdrawals = [e for e in report.events if e.kind is EventKind.withdrawal]
assert len(deposits) == 96 and len(withdrawals) == 5
assert all(e.amount > 0 and e.instrument is None for e in deposits)
assert all(e.amount < 0 and e.instrument is None for e in withdrawals)
assert sum(e.amount for e in withdrawals) == -Decimal("51294.41")
assert sum(e.amount for e in deposits) == Decimal("454058.45")
# --- 9. fees, taxes, refunds -----------------------------------------------------------
def test_fee_tax_and_refund_signs(report: ParsedReport) -> None:
fees = [e for e in report.events if e.kind is EventKind.commission]
taxes = [e for e in report.events if e.kind is EventKind.tax]
refunds = [e for e in report.events if e.kind is EventKind.tax_refund]
assert (len(fees), len(taxes), len(refunds)) == (7, 4, 2)
assert all(e.instrument is None for e in fees + taxes + refunds)
assert all(e.quantity is None and e.price is None for e in fees + taxes + refunds)
assert all(e.amount == Decimal(-45) and e.fee == Decimal(45) for e in fees)
assert [e.amount for e in taxes] == [Decimal(-234), Decimal(-3), Decimal(-22), Decimal(-32)]
assert [e.amount for e in refunds] == [Decimal(3), Decimal(32)]
assert all(e.tax == -e.amount for e in taxes)
assert all(e.tax == e.amount for e in refunds)
# --- unknown types and reconciliation warnings -----------------------------------------
def test_unknown_event_type_is_skipped_with_a_warning(parser: UniversalCsvParser) -> None:
"""Never `EventKind.other`: a silent bucket hides a format that grew a new row type."""
data = _csv(
'"MARGIN_CALL","2025-03-01 10:00:00","SBER","1","1","RUB","0","MCX","0","","False",""',
'"BUY","2025-03-01 10:00:00","SBER","100","1","RUB","0","MCX","0","","False",""',
)
result = parser.parse(data, "x.csv")
assert [e.kind for e in result.events] == [EventKind.buy]
assert result.meta["unknown_events"] == {"MARGIN_CALL": 1}
assert any("MARGIN_CALL" in w for w in result.warnings)
assert not any(e.kind is EventKind.other for e in result.events)
def test_closing_balances_are_absent_and_said_to_be(report: ParsedReport) -> None:
assert report.positions_end == []
assert report.cash_end == []
assert any("positions_end" in w for w in report.warnings)
def test_warns_that_the_target_account_is_the_user_s_to_choose(report: ParsedReport) -> None:
assert any("shadow" in w and "все брокерские счета" in w for w in report.warnings)
@@ -0,0 +1,56 @@
"""Реестр парсеров отчётов: какой файл достаётся какому парсеру (plan §2).
Проверка выглядит тривиальной, но ловит ровно ту ошибку, которую больше не поймает никто:
универсальный CSV узнаёт файл по набору колонок, то есть по определению шире любого
брокерского формата, и достаточно поставить его в списке первым, чтобы отчёт Сбера тихо
уехал в чужой парсер. Порядок в `PARSERS` — это поведение, а не оформление.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from fintracker.sources.reports import registry
FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "reports"
CASES = [
(FIXTURES / "sber" / "S930W42_11022026_17092026.html", "report_sber"),
(FIXTURES / "sber" / "S930W42_01082026_31082026.html", "report_sber"),
(FIXTURES / "vtb" / "reportd0235f1e-55ac-fdc8-5c7b-279e4fe26e2b.xlsx", "report_vtb"),
(FIXTURES / "vtb" / "report8ef8347a-2deb-e880-959a-d9a8ca12f735.xlsx", "report_vtb"),
(next((FIXTURES / "snowball").glob("*.csv")), "csv"),
]
@pytest.mark.parametrize("path,expected", CASES, ids=lambda v: getattr(v, "name", v))
def test_pick_routes_each_fixture_to_its_parser(path: Path, expected: str) -> None:
chosen = registry.pick(path.read_bytes(), path.name)
assert chosen is not None, f"{path.name}: ни один парсер не узнал файл"
assert chosen.name == expected
def test_every_registered_parser_is_reachable_by_name() -> None:
assert set(registry.names()) == {"report_sber", "report_vtb", "csv"}
for name in registry.names():
assert registry.get(name).name == name
def test_unknown_format_is_a_none_not_an_exception() -> None:
"""`pick` возвращает None, и роутер превращает это в 415 — не в 500."""
assert registry.pick(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n", "statement.pdf") is None
assert registry.pick(b"", "empty.txt") is None
def test_parser_metadata_is_filled_in() -> None:
"""`parser_name` и `parser_version` уходят в `raw_report_file` — пустых быть не должно."""
for parser in registry.PARSERS:
assert parser.name and parser.broker and parser.version
assert parser.formats
def test_csv_is_last_so_it_cannot_shadow_a_broker_format() -> None:
names = registry.names()
assert names.index("csv") == len(names) - 1
+362
View File
@@ -0,0 +1,362 @@
"""Парсер отчётов Сбера на обезличенных фикстурах (фаза 3).
Фикстуры — два отчёта по одному и тому же счёту: полный (11.02–17.09.2026) и августовский
(01.0831.08.2026). Пара выбрана не случайно: именно перекрывающиеся периоды ломают импорт,
если ключ дедупликации выводится из файла, а не из самой операции.
Главная проверка здесь — арифметическая. Отчёт печатает свои итоги («Итого, RUB» по
сделкам, «Пополнение счета» и «Исходящий остаток» в сводке), и распарсенные события обязаны
их воспроизвести: пополнения минус покупки плюс продажи = остаток на конец периода. Если
парсер потеряет строку, задвоит комиссию или примет расчётную строку за сделку, это
равенство разойдётся — а тест на «количество событий» этого не заметит.
"""
from __future__ import annotations
from collections import Counter
from decimal import Decimal
from pathlib import Path
import pytest
from fintracker.models.ledger import EventKind
from fintracker.sources.reports.base import BrokerEvent, ParsedReport
from fintracker.sources.reports.sber import SberHtmlParser, event_dedupe_key
FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "reports"
FULL = FIXTURES / "sber" / "S930W42_11022026_17092026.html"
AUGUST = FIXTURES / "sber" / "S930W42_01082026_31082026.html"
ACCOUNT = "S930W42"
#: Второй договор того же брокера — источник переводов д/с, не внешний поток.
OTHER_AGREEMENT = "8184V30"
ZERO = Decimal(0)
@pytest.fixture(scope="module")
def parser() -> SberHtmlParser:
return SberHtmlParser()
@pytest.fixture(scope="module")
def full(parser: SberHtmlParser) -> ParsedReport:
return parser.parse(FULL.read_bytes(), FULL.name)
@pytest.fixture(scope="module")
def august(parser: SberHtmlParser) -> ParsedReport:
return parser.parse(AUGUST.read_bytes(), AUGUST.name)
def total(events: list[BrokerEvent], kind: EventKind) -> Decimal:
return sum((e.amount for e in events if e.kind == kind), ZERO)
def of_kind(report: ParsedReport, kind: EventKind) -> list[BrokerEvent]:
return [e for e in report.events if e.kind == kind]
# --- 1. распознавание формата ------------------------------------------------------------
def test_sniff_accepts_both_sber_reports(parser: SberHtmlParser) -> None:
assert parser.sniff(FULL.read_bytes(), FULL.name)
assert parser.sniff(AUGUST.read_bytes(), AUGUST.name)
@pytest.mark.parametrize(
"path",
sorted((FIXTURES / "vtb").glob("*.xlsx")) + sorted((FIXTURES / "snowball").glob("*.csv")),
ids=lambda p: p.suffix,
)
def test_sniff_rejects_other_formats(parser: SberHtmlParser, path: Path) -> None:
assert not parser.sniff(path.read_bytes(), path.name)
def test_sniff_never_raises_on_garbage(parser: SberHtmlParser) -> None:
"""`registry.pick` опрашивает парсеры подряд — падение на чужом файле остановило бы перебор."""
assert not parser.sniff(b"", "empty.html")
assert not parser.sniff(b"\x00\x01\x02not html at all", "junk.html")
# --- 2. шапка и состав ---------------------------------------------------------------------
def test_full_report_header(full: ParsedReport) -> None:
assert full.broker == "sber"
assert full.account_external_id == ACCOUNT
assert (full.period_from.isoformat(), full.period_to.isoformat()) == (
"2026-02-11",
"2026-09-17",
)
assert full.meta["opened_at"] == "2026-02-11"
def test_august_report_header(august: ParsedReport) -> None:
assert august.account_external_id == ACCOUNT
assert (august.period_from.isoformat(), august.period_to.isoformat()) == (
"2026-08-01",
"2026-08-31",
)
def test_full_report_event_counts(full: ParsedReport) -> None:
assert Counter(e.kind for e in full.events) == {
EventKind.buy: 24,
EventKind.deposit: 9,
EventKind.sell: 2,
}
def test_august_report_event_counts(august: ParsedReport) -> None:
assert Counter(e.kind for e in august.events) == {
EventKind.buy: 2,
EventKind.deposit: 1,
}
# --- 3. арифметика: события воспроизводят итоги самого отчёта -------------------------------
def test_trade_totals_match_the_reports_own_total_row(full: ParsedReport) -> None:
"""«Итого, RUB» таблицы сделок: 89 205.58 оборота, 138.03 брокеру, 19.54 бирже."""
trades = of_kind(full, EventKind.buy) + of_kind(full, EventKind.sell)
assert len(trades) == 26
# Оборот — это «Сумма» сделки без комиссии, а `amount` её уже включает: у покупки
# прибавляет, у продажи вычитает. Отсюда знаки при обратном пересчёте.
turnover = sum(
(-e.amount - (e.fee or ZERO) if e.kind == EventKind.buy else e.amount + (e.fee or ZERO))
for e in trades
)
assert turnover == Decimal("89205.58")
assert sum((e.fee or ZERO for e in full.events), ZERO) == Decimal("138.03") + Decimal("19.54")
def test_deposits_match_the_summary_line(full: ParsedReport) -> None:
"""«Пополнение счета» сводки считает и переводы с другого договора — как и парсер."""
assert total(full.events, EventKind.deposit) == Decimal("49120.87")
assert Decimal(full.meta["summary"]["Пополнение счета"]) == Decimal("49120.87")
def test_cash_closes_against_the_reported_balance(full: ParsedReport) -> None:
"""Сквозная проверка: сумма денежных эффектов всех событий = исходящий остаток.
Это единственная проверка, которая ловит и потерянную строку, и лишнюю: пропущенная
сделка занижает отток, а расчётная строка «Сделка от …», принятая за сделку, задваивает
его. Входящий остаток нулевой, поэтому сумма событий и есть конечный остаток.
"""
assert Decimal(full.meta["summary"]["Входящий остаток"]) == ZERO
assert sum((e.amount for e in full.events), ZERO) == Decimal("3171.34")
rub = next(c for c in full.cash_end if c.currency == "RUB")
assert rub.balance == Decimal("3171.34")
def test_august_cash_closes_from_its_own_opening_balance(august: ParsedReport) -> None:
opening = Decimal(august.meta["summary"]["Входящий остаток"])
assert opening == Decimal("357.15")
rub = next(c for c in august.cash_end if c.currency == "RUB")
assert opening + sum((e.amount for e in august.events), ZERO) == rub.balance
# --- 4. комиссии учтены ровно один раз -----------------------------------------------------
def test_commission_is_capitalised_and_never_emitted_separately(full: ParsedReport) -> None:
"""Комиссия живёт в `fee` сделки; отдельных `commission`-событий быть не должно.
`ledger/lots.py` капитализирует `fee` в стоимость лота, поэтому вторая, «денежная»
ипостась той же комиссии (строки «Комиссия Брокера от …» в движении денег) ушла бы в
леджер повторно — как отток, которого не было.
"""
assert not of_kind(full, EventKind.commission)
assert all(e.fee is None for e in full.events if e.kind == EventKind.deposit)
fees = sum((e.fee or ZERO for e in full.events), ZERO)
assert fees == Decimal("157.57")
assert Decimal(full.meta["summary"]["Комиссия брокера"]) == Decimal("-138.03")
assert Decimal(full.meta["summary"]["Комиссия биржи"]) == Decimal("-19.54")
def test_parser_reports_no_fee_divergence(full: ParsedReport, august: ParsedReport) -> None:
"""Парсер сам сверяет две ипостаси комиссии и предупреждает при расхождении."""
for report in (full, august):
assert not [w for w in report.warnings if "комиссии расходятся" in w]
# --- 5. дедупликация перекрывающихся периодов ----------------------------------------------
def keys(report: ParsedReport) -> dict[str, BrokerEvent]:
return {event_dedupe_key(report, e): e for e in report.events}
def test_august_events_keep_their_keys_in_the_full_report(
full: ParsedReport, august: ParsedReport
) -> None:
"""Каждое августовское событие должно прийти с тем же ключом из полного отчёта.
Иначе импорт двух пересекающихся отчётов задвоит август — ровно тот случай, который
план §1.6 A называет «перекрывающиеся периоды апсертят в ту же строку».
"""
full_keys, august_keys = keys(full), keys(august)
assert set(august_keys) <= set(full_keys), (
"у августовских событий появились ключи, которых нет в полном отчёте: "
f"{sorted(set(august_keys) - set(full_keys))}"
)
for key, event in august_keys.items():
twin = full_keys[key]
assert (event.kind, event.trade_date, event.quantity, event.price, event.amount) == (
twin.kind,
twin.trade_date,
twin.quantity,
twin.price,
twin.amount,
), f"ключ {key} указывает на разные операции"
def test_overlap_covers_every_august_operation(august: ParsedReport) -> None:
"""Перекрытие не должно оказаться пустым — иначе предыдущий тест проходит вхолостую."""
assert len(keys(august)) == len(august.events) == 3
def test_trades_key_on_the_brokers_deal_number(full: ParsedReport) -> None:
trades = of_kind(full, EventKind.buy) + of_kind(full, EventKind.sell)
assert all(e.trade_no for e in trades)
assert len({e.trade_no for e in trades}) == len(trades)
def test_cash_rows_have_stable_keys_without_a_deal_number(full: ParsedReport) -> None:
"""У денежных строк номера нет, и ключ обязан оставаться уникальным внутри отчёта."""
deposits = of_kind(full, EventKind.deposit)
assert all(e.trade_no is None for e in deposits)
assert len({event_dedupe_key(full, e) for e in deposits}) == len(deposits)
def test_reparsing_the_same_bytes_is_deterministic(parser: SberHtmlParser) -> None:
once = parser.parse(FULL.read_bytes(), FULL.name)
twice = parser.parse(FULL.read_bytes(), FULL.name)
assert [event_dedupe_key(once, e) for e in once.events] == [
event_dedupe_key(twice, e) for e in twice.events
]
# --- 6. закрывающие позиции и остатки ------------------------------------------------------
def test_closing_positions(full: ParsedReport) -> None:
assert len(full.positions_end) == 10
assert sum((p.market_value or ZERO for p in full.positions_end), ZERO) == Decimal("46663.30")
aeroflot = next(p for p in full.positions_end if p.instrument.isin == "RU0009062285")
assert aeroflot.qty == Decimal("130")
assert aeroflot.currency == "RUB"
def test_closing_cash_keeps_every_currency(full: ParsedReport) -> None:
balances = {c.currency: c.balance for c in full.cash_end}
assert balances == {"RUB": Decimal("3171.34"), "EUR": ZERO, "USD": ZERO}
def test_august_closing_snapshot(august: ParsedReport) -> None:
assert len(august.positions_end) == 5
assert sum((p.market_value or ZERO for p in august.positions_end), ZERO) == Decimal("38739.43")
assert next(c for c in august.cash_end if c.currency == "RUB").balance == Decimal("1407.88")
# --- 7. справочник инструментов ------------------------------------------------------------
def test_securities_directory_feeds_instruments(full: ParsedReport) -> None:
assert len(full.instruments) >= 9
assert all(i.isin and i.ticker for i in full.instruments)
assert all(i.asset_class_hint for i in full.instruments)
sber = next(i for i in full.instruments if i.ticker == "SBER")
assert (sber.isin, sber.asset_class_hint) == ("RU0009029540", "share")
fund = next(i for i in full.instruments if i.ticker == "STME")
assert fund.asset_class_hint in {"etf", "fund"}
def test_trades_reference_instruments_by_isin(full: ParsedReport) -> None:
"""Сделка печатает только тикер — ISIN подставляется из справочника того же файла."""
trades = of_kind(full, EventKind.buy) + of_kind(full, EventKind.sell)
assert all(e.instrument is not None and e.instrument.isin for e in trades)
# --- 8. ловушки формата --------------------------------------------------------------------
def test_iis_contributions_table_is_read_but_not_emitted(
full: ParsedReport, august: ParsedReport
) -> None:
"""Таблица зачислений на ИИС кумулятивна за ГОД, а не за период отчёта.
Августовский файл перечисляет в ней восемь пополнений начиная с февраля — всё, что
случилось до даты его формирования (01.09.2026); в полном отчёте их девять, добавилось
сентябрьское. То есть таблица растёт от отчёта к отчёту независимо от периода, и если
бы парсер эмитил её строки, импорт августовского файла поверх полного задвоил бы
пополнения за полгода. Поэтому её содержимое только пересчитывается, в события не идёт:
августовский отчёт даёт ровно одно денежное поступление — то, что реально было в августе.
"""
assert full.meta["iis_contributions_ignored"] == 9
assert august.meta["iis_contributions_ignored"] == 8
assert len(of_kind(august, EventKind.deposit)) == 1
def test_settlement_lines_are_not_mistaken_for_trades(full: ParsedReport) -> None:
"""«Сделка от DD.MM.YYYY» в движении денег — расчёт по уже учтённой сделке."""
assert not [e for e in full.events if (e.description or "").startswith("Сделка от")]
def test_transfer_from_another_agreement_keeps_its_counterparty(full: ParsedReport) -> None:
"""Перевод с другого договора Сбера — deposit, но с сохранённым источником.
Без `internal_transfer_from` эти деньги навсегда останутся внешним потоком: заведи
пользователь второй договор счётом, XIRR увидел бы пополнение здесь и вывод там как два
независимых события.
"""
transfers = [
e
for e in of_kind(full, EventKind.deposit)
if e.meta.get("internal_transfer_from") == OTHER_AGREEMENT
]
assert len(transfers) == 2
assert sum((e.amount for e in transfers), ZERO) == Decimal("3458.88") + Decimal("10551.18")
def test_missing_coupon_section_is_reported_not_assumed(full: ParsedReport) -> None:
"""Раздела «Купонный доход» в этих отчётах нет — это факт для data quality, не молчание."""
assert any("Купонный доход" in w for w in full.warnings)
# --- 9. деньги --------------------------------------------------------------------------------
def numbers(report: ParsedReport) -> list[object]:
values: list[object] = []
for e in report.events:
values += [e.amount, e.quantity, e.price, e.fee, e.tax, e.accrued_interest]
for p in report.positions_end:
values += [p.qty, p.price, p.market_value, p.accrued_interest]
values += [c.balance for c in report.cash_end]
return values
@pytest.mark.parametrize("name", ["full", "august"])
def test_no_floats_anywhere(name: str, request: pytest.FixtureRequest) -> None:
"""Деньги — только Decimal (AGENTS.md). Один float здесь означает потерю копеек ниже."""
report: ParsedReport = request.getfixturevalue(name)
assert not [v for v in numbers(report) if isinstance(v, float)]
assert all(isinstance(v, Decimal) for v in numbers(report) if v is not None)
def test_every_event_carries_currency_and_traceability(full: ParsedReport) -> None:
for event in full.events:
assert event.currency == "RUB"
assert event.raw_line_no > 0
assert event.meta.get("section")
+410
View File
@@ -0,0 +1,410 @@
"""VTB xlsx report parser against the two anonymised fixtures.
The fixtures are the same account rendered twice: the whole life of the IIS (12.0217.09) and
the single month of August, which is a strict subset of it. That overlap is the point — it is
exactly the case the dedupe key exists for, so the deal both files contain must produce one
key, and the monthly file must not invent a trade the full one does not have.
Every number asserted here is taken from «Сводная информация по субсчёту Клиента», the
broker's own totals block: the parser is trusted only insofar as what it extracted adds up to
what VTB printed.
"""
from __future__ import annotations
import io
import zipfile
from collections import Counter
from dataclasses import fields, is_dataclass
from decimal import Decimal
from pathlib import Path
import pytest
from openpyxl import load_workbook
from fintracker.models.ledger import EventKind
from fintracker.sources.reports.base import ParsedReport
from fintracker.sources.reports.vtb import common
from fintracker.sources.reports.vtb.xlsx import VtbXlsxParser
FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "reports"
FULL = FIXTURES / "vtb" / "reportd0235f1e-55ac-fdc8-5c7b-279e4fe26e2b.xlsx"
AUGUST = FIXTURES / "vtb" / "report8ef8347a-2deb-e880-959a-d9a8ca12f735.xlsx"
#: The August deal, as printed in both files: «№ сделки» B17476399721, 33 EQMX at 122.9.
AUGUST_TRADE_NO = "B17476399721"
@pytest.fixture(scope="module")
def parser() -> VtbXlsxParser:
return VtbXlsxParser()
@pytest.fixture(scope="module")
def full(parser: VtbXlsxParser) -> ParsedReport:
return parser.parse(FULL.read_bytes(), FULL.name)
@pytest.fixture(scope="module")
def august(parser: VtbXlsxParser) -> ParsedReport:
return parser.parse(AUGUST.read_bytes(), AUGUST.name)
def trades(report: ParsedReport) -> list:
return [event for event in report.events if event.kind in {EventKind.buy, EventKind.sell}]
def count_security_movement_rows(path: Path) -> int:
"""Rows of «Движение ценных бумаг» counted straight off the sheet.
Deliberately independent of the parser: the section is the settlement side of the same
deals, so its row count is an outside check that no trade was dropped or doubled.
"""
workbook = load_workbook(io.BytesIO(path.read_bytes()), read_only=True, data_only=True)
sheet = workbook["brokerage_report"]
sheet.reset_dimensions()
inside = False
rows = 0
for row in sheet.iter_rows(values_only=True):
label = common.normalize_text(row[1] if len(row) > 1 else None)
if label == "движение ценных бумаг":
inside = True
continue
if not inside:
continue
if label in {"заключенные в отчетном периоде сделки с ценными бумагами"}:
break
if "расчеты по заключенным сделкам" in common.normalize_text(
row[16] if len(row) > 16 else None
):
rows += 1
workbook.close()
return rows
# -- 1. sniff ----------------------------------------------------------------------------
def test_sniff_accepts_both_vtb_files(parser: VtbXlsxParser) -> None:
for path in (FULL, AUGUST):
assert parser.sniff(path.read_bytes(), path.name) is True
def test_sniff_rejects_other_brokers(parser: VtbXlsxParser) -> None:
others = list((FIXTURES / "sber").glob("*.html")) + list((FIXTURES / "snowball").glob("*.csv"))
assert others, "фикстуры других брокеров не найдены — проверка бессмысленна"
for path in others:
assert parser.sniff(path.read_bytes(), path.name) is False
def test_sniff_never_raises_on_garbage(parser: VtbXlsxParser) -> None:
"""A registry walks every parser over every upload, so a False must never be an
exception — including for a zip that merely looks like a workbook."""
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w") as archive:
archive.writestr("xl/workbook.xml", "<workbook/>")
for payload in (b"", b"not a zip", b"PK\x03\x04broken", buffer.getvalue()):
assert parser.sniff(payload, "whatever.xlsx") is False
def test_sniff_ignores_the_file_name(parser: VtbXlsxParser) -> None:
"""The name is a GUID with no format in it; content decides."""
assert parser.sniff(FULL.read_bytes(), "totally-unrelated.bin") is True
# -- 2. the full report ------------------------------------------------------------------
def test_full_report_header(full: ParsedReport) -> None:
assert full.broker == "vtb"
assert full.account_external_id == "77R593"
assert (full.period_from.isoformat(), full.period_to.isoformat()) == (
"2026-02-12",
"2026-09-17",
)
assert full.meta["iis_opened_at"] == "2026-02-12"
def test_full_report_trades_come_only_from_concluded_section(full: ParsedReport) -> None:
"""«Завершенные» repeats «Заключенные» with the same deal numbers, and «Движение ценных
бумаг» settles those same deals — so ten deals must yield ten events, not twenty or
thirty."""
numbers = [event.trade_no for event in trades(full)]
assert len(numbers) == len(set(numbers)) == count_security_movement_rows(FULL) == 10
def test_full_report_parses_without_warnings(full: ParsedReport) -> None:
assert full.warnings == []
# -- 3. money ----------------------------------------------------------------------------
def test_full_report_deposits_and_fees(full: ParsedReport) -> None:
deposits = [event for event in full.events if event.kind is EventKind.deposit]
assert sum(event.amount for event in deposits) == Decimal("26100")
assert all(event.amount > 0 for event in deposits), "пополнение увеличивает счёт"
# The per-deal commissions add up to «Вознаграждение брокера» of the summary block to the
# kopeck, which is why they are capitalised into the deals instead of being emitted
# separately: `ledger/lots.py` already puts `fee` into the cost of the lot.
assert sum((event.fee or Decimal(0)) for event in full.events) == Decimal("3.64")
assert Decimal(full.meta["summary"]["вознаграждение брокера"]) == Decimal("-3.64")
assert not [event for event in full.events if event.kind is EventKind.commission]
def test_full_report_signs(full: ParsedReport) -> None:
for event in trades(full):
assert event.quantity is not None
if event.kind is EventKind.buy:
assert event.quantity > 0 and event.amount < 0
else:
assert event.quantity < 0 and event.amount > 0
def test_settlement_saldo_is_not_emitted_twice(full: ParsedReport) -> None:
"""«Сальдо расчетов по сделкам с ценными бумагами» is the money leg of the trades; the
cash section must contribute deposits only."""
cash_kinds = Counter(event.kind for event in full.events if event.meta.get("section") == "cash")
assert cash_kinds == Counter({EventKind.deposit: 8})
def test_closing_cash_follows_from_the_events(full: ParsedReport) -> None:
"""The whole-file check: opening + everything emitted = closing, as VTB states it."""
summary = full.meta["summary"]
opening = Decimal(summary["входящий остаток денежных средств"])
closing = Decimal(summary["исходящий остаток денежных средств"])
assert opening + sum(event.amount for event in full.events) == closing == Decimal("120.99")
# -- 4. positions and cash balances -------------------------------------------------------
def test_full_report_positions_end(full: ParsedReport) -> None:
positions = {position.instrument.isin: position for position in full.positions_end}
assert set(positions) == {"RU000A101EJ5", "RU000A1014L8"}
assert positions["RU000A101EJ5"].qty == Decimal("197")
assert positions["RU000A1014L8"].qty == Decimal("0")
eqmx = positions["RU000A101EJ5"].instrument
assert (eqmx.ticker, eqmx.asset_class_hint) == ("EQMX", "etf")
assert eqmx.name == "EQMX ETF"
assert eqmx.meta["reg_number"] == "3965"
assert positions["RU000A101EJ5"].price == Decimal("124.653")
def test_currency_is_normalised_to_iso(full: ParsedReport) -> None:
"""VTB prints the pre-ISO `RUR`; the ledger stores three-letter ISO codes only."""
assert [(cash.currency, cash.balance) for cash in full.cash_end] == [("RUB", Decimal("120.99"))]
codes = {cash.currency for cash in full.cash_end}
codes |= {position.currency for position in full.positions_end}
codes |= {event.currency for event in full.events}
codes |= {event.price_currency for event in trades(full)}
assert codes == {"RUB"}
def test_instrument_hint_reaches_trade_events(full: ParsedReport) -> None:
"""Only the positions table names the asset class («ПАЙ»); the trades table does not, so
the hint is carried over by ISIN."""
assert {ref.asset_class_hint for ref in full.instruments} == {"etf"}
assert all(
event.instrument is not None and event.instrument.asset_class_hint == "etf"
for event in trades(full)
)
# -- 5. the monthly report and the cross-file dedupe key ----------------------------------
def test_august_report(august: ParsedReport) -> None:
assert (august.period_from.isoformat(), august.period_to.isoformat()) == (
"2026-08-01",
"2026-08-31",
)
assert august.account_external_id == "77R593"
assert august.warnings == []
deals = trades(august)
assert len(deals) == 1
deal = deals[0]
assert deal.kind is EventKind.buy
assert deal.instrument is not None and deal.instrument.isin == "RU000A101EJ5"
assert deal.quantity == Decimal("33")
assert deal.price == Decimal("122.9")
assert deal.trade_no == AUGUST_TRADE_NO
# 33 * 122.9 = 4055.70 plus the 0.81 commission capitalised into the deal.
assert deal.amount == Decimal("-4056.51")
assert deal.fee == Decimal("0.81")
def test_same_deal_has_one_dedupe_key_in_both_reports(
full: ParsedReport, august: ParsedReport
) -> None:
"""The overlapping-period case from plan §1.6 A: re-importing August after the full
report must upsert, not duplicate."""
keys = {}
for report in (full, august):
for event in trades(report):
if event.trade_no == AUGUST_TRADE_NO:
keys[report.period_to] = common.dedupe_key(report.account_external_id, event)
assert len(keys) == 2
assert len(set(keys.values())) == 1
def test_dedupe_key_uses_the_brokers_own_deal_number(full: ParsedReport) -> None:
"""«№ сделки» (Z), not «№ сделки у организатора торгов» (AC).
The key is `sha1(broker | account | trade_no)` and is therefore already unique within the
broker; what it needs is the number VTB prints in every rendering of its own report. The
organiser's number is a different string (`17476399721` against `B17476399721`) and is
absent for over-the-counter deals in some formats.
"""
deal = next(event for event in trades(full) if event.trade_no == AUGUST_TRADE_NO)
assert deal.meta["exchange_trade_no"] == "17476399721"
assert common.dedupe_key("77R593", deal) != common.dedupe_key(
"77R593",
type(deal)(
**{
**{field.name: getattr(deal, field.name) for field in fields(deal)},
"trade_no": deal.meta["exchange_trade_no"],
}
),
)
def test_cash_rows_get_distinct_fingerprint_keys(full: ParsedReport) -> None:
"""Deposits carry no deal number and fall back to a fingerprint; two 4 000 ₽ top-ups in
different months must still differ."""
keys = [
common.dedupe_key(full.account_external_id, event)
for event in full.events
if event.kind is EventKind.deposit
]
assert len(set(keys)) == len(keys) == 8
# -- 6. concluded vs completed ------------------------------------------------------------
def test_concluded_and_completed_deal_numbers_agree(full: ParsedReport) -> None:
"""Asserted through the parser's own warning channel: a divergence is reported, and for a
fully settled period there is none."""
assert not [warning for warning in full.warnings if "Завершенных" in warning]
parsed = VtbXlsxParser().parse(FULL.read_bytes(), FULL.name)
concluded = {event.trade_no for event in trades(parsed)}
assert concluded == set(_completed_deal_numbers(FULL))
def _completed_deal_numbers(path: Path) -> list[str]:
"""«№ сделки» of «Завершенные…» read straight off the sheet, bypassing the parser."""
workbook = load_workbook(io.BytesIO(path.read_bytes()), read_only=True, data_only=True)
sheet = workbook["brokerage_report"]
sheet.reset_dimensions()
inside = False
numbers: list[str] = []
for row in sheet.iter_rows(values_only=True):
label = common.normalize_text(row[1] if len(row) > 1 else None)
if label.startswith("завершенные в отчетном периоде сделки с ценными бумагами"):
inside = True
continue
if not inside:
continue
value = row[25] if len(row) > 25 else None # column Z
if value and str(value).strip() and common.normalize_text(value) != "№ сделки":
numbers.append(str(value).strip())
workbook.close()
return numbers
# -- 7. no floats anywhere ----------------------------------------------------------------
def _floats(value: object, path: str = "") -> list[str]:
if isinstance(value, bool) or value is None:
return []
if isinstance(value, float):
return [path]
if is_dataclass(value) and not isinstance(value, type):
found: list[str] = []
for field in fields(value):
found += _floats(getattr(value, field.name), f"{path}.{field.name}")
return found
if isinstance(value, dict):
return [p for key, item in value.items() for p in _floats(item, f"{path}[{key}]")]
if isinstance(value, (list, tuple, set)):
return [p for index, item in enumerate(value) for p in _floats(item, f"{path}[{index}]")]
return []
@pytest.mark.parametrize("name", ["full", "august"])
def test_no_floats_in_output(name: str, request: pytest.FixtureRequest) -> None:
"""openpyxl hands back floats for every numeric cell; none of them may survive into the
result, or a kopeck disappears into binary rounding on the way to `NUMERIC(24,10)`."""
report: ParsedReport = request.getfixturevalue(name)
assert _floats(report, name) == []
# -- 8. totals against the summary block --------------------------------------------------
@pytest.mark.parametrize(("name", "expected"), [("full", "-25975.37"), ("august", "-4055.7")])
def test_trade_sums_match_the_settlement_saldo(
name: str, expected: str, request: pytest.FixtureRequest
) -> None:
"""Σ «Сумма сделки» (column M) of the purchases minus the sales equals «Сальдо расчетов по
сделкам с ценными бумагами» in the summary.
The commission has to be taken back out of `amount` to make the comparison: VTB's saldo is
the money of the deals themselves, while `event.amount` carries the fee inside it by the
ledger's convention. The two agree exactly once that is done — the fee lives in the same
file as its own «Вознаграждение брокера» line, and it is that line, not the saldo, that
accounts for it.
"""
report: ParsedReport = request.getfixturevalue(name)
gross = Decimal(0)
for event in trades(report):
fee = event.fee or Decimal(0)
gross += event.amount + fee if event.kind is EventKind.buy else event.amount - fee
assert gross == Decimal(expected)
assert gross == Decimal(report.meta["summary"]["сальдо расчетов по сделкам с ценными бумагами"])
def test_position_quantities_match_the_settled_movements(full: ParsedReport) -> None:
"""Closing position = what the trades did, since the account opened empty."""
by_isin: dict[str, Decimal] = {}
for event in trades(full):
assert event.instrument is not None and event.instrument.isin
assert event.quantity is not None
by_isin[event.instrument.isin] = (
by_isin.get(event.instrument.isin, Decimal(0)) + event.quantity
)
for position in full.positions_end:
assert position.instrument.isin is not None
assert by_isin[position.instrument.isin] == position.qty
# -- parse errors --------------------------------------------------------------------------
def test_unreadable_file_raises_parse_error(parser: VtbXlsxParser) -> None:
from fintracker.sources.reports.base import ParseError
with pytest.raises(ParseError):
parser.parse(b"PK\x03\x04 definitely not a workbook", "report.xlsx")
def test_workbook_without_a_period_raises_parse_error(parser: VtbXlsxParser) -> None:
"""A file we recognise but cannot place on a timeline must fail loudly rather than import
as an empty report."""
from openpyxl import Workbook
from fintracker.sources.reports.base import ParseError
workbook = Workbook()
sheet = workbook.worksheets[0]
sheet.title = "brokerage_report"
sheet["D1"] = "Отчет Банка ВТБ (ПАО) без периода"
buffer = io.BytesIO()
workbook.save(buffer)
with pytest.raises(ParseError):
parser.parse(buffer.getvalue(), "report.xlsx")
+168
View File
@@ -0,0 +1,168 @@
"""Персданные не должны попадать в git (AGENTS.md).
Отчёты Сбера и ВТБ содержат ФИО, ИНН, номер соглашения и номер лицевого счёта. В
репозиторий идут только обезличенные копии, и этот тест — то, что делает правило
исполнимым, а не пожеланием: он падает, если в `tests/fixtures/reports/` (вне `raw/`)
встречается что-то персональное.
Две проверки, и вторая важнее первой:
1. **По форме.** 12-значный ИНН, 20-значный счёт, ФИО кириллицей — ловятся регулярками и
без доступа к оригиналу, то есть работают и в CI, где `raw/` нет.
2. **По содержанию.** Если `raw/` на месте, из оригиналов извлекаются те же секреты, что
вырезает `anonymize.py`, и каждый проверяется на отсутствие в каждой фикстуре и в её
имени. Это ловит случай, который первая проверка пропустит: фикстуру, собранную руками
мимо скрипта.
"""
from __future__ import annotations
import io
import re
import zipfile
from pathlib import Path
import pytest
from fintracker.sources.reports.anonymize import (
FAKE_NAME,
FAKE_PATRONYMIC,
FAKE_SURNAME,
RE_ACCOUNT_20,
RE_CAPS_FIO,
RE_INN,
RE_PLACEHOLDER_NUMBER,
RE_SHORT_NAME,
Secrets,
collect_secrets,
)
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "reports"
RAW = FIXTURES / "raw"
ALLOWED_NAMES = {
f"{FAKE_SURNAME} {FAKE_NAME[0]}. {FAKE_PATRONYMIC[0]}.",
f"{FAKE_SURNAME.upper()} {FAKE_NAME.upper()} {FAKE_PATRONYMIC.upper()}",
}
def committed_fixtures() -> list[Path]:
"""Every fixture that git tracks: everything under reports/ except raw/."""
if not FIXTURES.exists():
return []
return sorted(
p
for p in FIXTURES.rglob("*")
if p.is_file() and RAW not in p.parents and p != RAW and not p.name.startswith(".")
)
def raw_reports() -> list[Path]:
if not RAW.exists():
return []
return sorted(p for p in RAW.rglob("*") if p.is_file() and not p.name.startswith("."))
def raw_secrets() -> Secrets:
"""The same corpus-wide secret set the script uses — never a per-file one."""
return collect_secrets((p.read_bytes(), p.name) for p in raw_reports())
def readable_text(path: Path) -> str:
"""The visible text of a fixture, whatever its container."""
data = path.read_bytes()
if path.suffix.lower() in {".xlsx", ".xlsm"}:
archive = zipfile.ZipFile(io.BytesIO(data))
parts = [
archive.read(name).decode("utf-8", "replace")
for name in archive.namelist()
if name.startswith("xl/") and name.endswith(".xml")
]
return "\n".join(parts)
return data.decode("utf-8", "replace")
@pytest.mark.parametrize("path", committed_fixtures(), ids=lambda p: str(p.name))
def test_fixture_has_no_personal_shapes(path: Path) -> None:
text = readable_text(path)
inns = [v for v in RE_INN.findall(text) if not RE_PLACEHOLDER_NUMBER.match(v)]
assert not inns, f"{path.name}: похоже на ИНН физлица: {inns[:3]}"
accounts = [v for v in RE_ACCOUNT_20.findall(text) if not RE_PLACEHOLDER_NUMBER.match(v)]
assert not accounts, f"{path.name}: похоже на номер счёта: {accounts[:3]}"
names = {m.group(0) for m in RE_SHORT_NAME.finditer(text)} - ALLOWED_NAMES
assert not names, f"{path.name}: ФИО с инициалами: {sorted(names)[:3]}"
caps = {m.group(0) for m in RE_CAPS_FIO.finditer(text)} - ALLOWED_NAMES
assert not caps, f"{path.name}: ФИО заглавными: {sorted(caps)[:3]}"
@pytest.mark.skipif(not RAW.exists(), reason="реальных отчётов нет (CI) — проверка по форме")
def test_no_secret_from_raw_leaks_into_fixtures() -> None:
"""Ни один секрет, извлечённый из оригиналов, не встречается в коммитимых фикстурах."""
secrets = set(raw_secrets().originals)
if not secrets:
pytest.skip("в оригиналах не нашлось персданных — нечего проверять")
leaks: list[str] = []
for fixture in committed_fixtures():
text = readable_text(fixture)
for secret in secrets:
if secret in text:
leaks.append(f"{fixture.name}: содержимое содержит {secret!r}")
if secret in fixture.name:
leaks.append(f"{fixture.name}: имя файла содержит {secret!r}")
assert not leaks, "утечка персданных в git:\n" + "\n".join(leaks)
@pytest.mark.skipif(not RAW.exists(), reason="реальных отчётов нет")
def test_fixtures_are_reproducible_from_the_script() -> None:
"""Фикстуры совпадают с тем, что сейчас выдаёт скрипт: обезличивание воспроизводимо."""
from fintracker.sources.reports.anonymize import anonymize, anonymize_filename
secrets = raw_secrets()
mismatched: list[str] = []
for source in raw_reports():
clean, _ = anonymize(source.read_bytes(), source.name, secrets)
relative = source.relative_to(RAW)
target = FIXTURES / relative.parent / anonymize_filename(source.name, secrets)
if not target.exists():
mismatched.append(f"нет фикстуры {target.name} для {relative}")
elif target.read_bytes() != clean:
mismatched.append(f"{target.name} не совпадает с выводом скрипта")
assert not mismatched, (
"перегенерируйте: uv run python scripts/anonymize_reports.py\n" + "\n".join(mismatched)
)
def test_raw_reports_are_not_tracked_by_git() -> None:
"""`raw/` в .gitignore — иначе всё остальное бессмысленно."""
import subprocess
root = Path(__file__).resolve().parents[2]
out = subprocess.run(
["git", "ls-files", "backend/tests/fixtures/reports/raw"],
cwd=root,
capture_output=True,
text=True,
check=False,
)
tracked = [
line
for line in out.stdout.splitlines()
if line.strip() and not line.rsplit("/", 1)[-1].startswith(".")
]
assert not tracked, f"реальные отчёты под контролем версий: {tracked}"
RE_ANY_CYRILLIC_FIO = re.compile(r"[А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]+(?:ович|евич|овна|евна)\b")
@pytest.mark.parametrize("path", committed_fixtures(), ids=lambda p: str(p.name))
def test_fixture_has_no_full_patronymic(path: Path) -> None:
"""«Иванов Пётр Сергеевич» в обычном регистре — отдельный случай, регистр не спасает."""
found = {m.group(0) for m in RE_ANY_CYRILLIC_FIO.finditer(readable_text(path))}
found -= {f"{FAKE_NAME} {FAKE_PATRONYMIC}", f"{FAKE_SURNAME} {FAKE_PATRONYMIC}"}
assert not found, f"{path.name}: ФИО с отчеством: {sorted(found)[:3]}"