{"id":483560,"date":"2026-06-14T09:54:37","date_gmt":"2026-06-14T09:54:37","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=483560"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=483560","title":{"rendered":"\u041a\u0430\u043a \u044f \u0441\u043e\u0431\u0440\u0430\u043b \u044d\u0442\u0430\u043b\u043e\u043d\u043d\u044b\u0439 Data Engineering \u043f\u0440\u043e\u0435\u043a\u0442: ClickHouse, Kafka, Spark, dbt, Airflow \u0438 Superset \u0437\u0430 \u043e\u0434\u043d\u0443 \u043a\u043e\u043c\u0430\u043d\u0434\u0443"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041a\u043e\u0433\u0434\u0430 \u044f \u0438\u0441\u043a\u0430\u043b \u0443\u0447\u0435\u0431\u043d\u044b\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u044b \u043f\u043e data engineering, \u043a\u0430\u0440\u0442\u0438\u043d\u0430 \u0431\u044b\u043b\u0430 \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e\u0439: \u043b\u0438\u0431\u043e \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b \u043d\u0430 \u0434\u0432\u0430 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0430 (\u00ab\u043f\u0438\u0448\u0435\u043c \u0432 Kafka, \u0447\u0438\u0442\u0430\u0435\u043c \u0432 Spark\u00bb), \u043b\u0438\u0431\u043e enterprise-\u0441\u0445\u0435\u043c\u0430 \u0431\u0435\u0437 \u0435\u0434\u0438\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u0447\u043a\u0438 \u043a\u043e\u0434\u0430. \u041c\u043d\u0435 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0447\u0435\u0433\u043e-\u0442\u043e \u0441\u0440\u0435\u0434\u043d\u0435\u0433\u043e \u2014 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0442\u0435\u043a, \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b, \u043d\u043e \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0432\u0441\u0451 \u043f\u043e\u0434\u043d\u0438\u043c\u0430\u0435\u0442\u0441\u044f \u043e\u0434\u043d\u043e\u0439 \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439 <code>make deploy<\/code> \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.<\/p>\n<p>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442: \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u0441\u0431\u043e\u0440\u0430 \u0438 \u0430\u043d\u0430\u043b\u0438\u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u043e\u0432\u0430\u043b\u044e\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u043e\u0436\u043d\u043e \u0441\u043a\u043b\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0430 \u043b\u044e\u0431\u043e\u0439 \u043c\u0430\u0448\u0438\u043d\u0435 \u0441 Docker. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043e\u0431 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435, \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0445 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0440\u0435\u0448\u0435\u043d\u0438\u044f\u0445 \u0438 \u0433\u0440\u0430\u0431\u043b\u044f\u0445, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0432\u0441\u0442\u0440\u0435\u0442\u0438\u043b\u0438\u0441\u044c \u043f\u043e \u043f\u0443\u0442\u0438.<\/p>\n<hr\/>\n<h3>\u0427\u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c<\/h3>\n<p>\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u044e \u0441\u043d\u0430\u0447\u0430\u043b\u0430, \u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u0430:<\/p>\n<ul>\n<li>\n<p>\u0421\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 30 \u0434\u043d\u0435\u0439 \u0438\u0441\u0442\u043e\u0440\u0438\u0438 \u043f\u043e 5 \u0441\u0438\u043c\u0432\u043e\u043b\u0430\u043c (BTC\/ETH\/SOL\/BNB\/XRP) \u0432 \u043c\u043e\u043c\u0435\u043d\u0442 \u0434\u0435\u043f\u043b\u043e\u044f<\/p>\n<\/li>\n<li>\n<p>\u0421\u0442\u0440\u0438\u043c\u0438\u0442 live-\u0434\u0430\u043d\u043d\u044b\u0435 \u0441 Binance \u0447\u0435\u0440\u0435\u0437 WebSocket \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0430\u043d\u043e\u043c\u0430\u043b\u0438\u0438: \u0432\u0441\u043f\u043b\u0435\u0441\u043a\u0438 \u043e\u0431\u044a\u0451\u043c\u0430 \u0438 \u0430\u043d\u043e\u043c\u0430\u043b\u044c\u043d\u043e \u043a\u0440\u0443\u043f\u043d\u044b\u0435 \u0441\u0432\u0435\u0447\u0438<\/p>\n<\/li>\n<li>\n<p>\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 rolling volatility \u0438 \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u0440\u044b\u043d\u043e\u0447\u043d\u044b\u0435 \u0440\u0435\u0436\u0438\u043c\u044b \u0447\u0435\u0440\u0435\u0437 Apache Spark<\/p>\n<\/li>\n<li>\n<p>\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u0442 \u0432\u0441\u0451 \u043d\u0430 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u043c \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u0435 \u2014 \u0442\u043e\u0436\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438<\/p>\n<\/li>\n<\/ul>\n<p>\u0418 \u0432\u0435\u0441\u044c \u044d\u0442\u043e\u0442 \u0441\u0442\u0435\u043a \u043f\u043e\u0434\u043d\u0438\u043c\u0430\u0435\u0442\u0441\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439:<\/p>\n<pre><code class=\"bash\">git clone https:\/\/github.com\/andreivolokhovskii-coder\/Crypto-Research-Workbench.gitcd Crypto-Research-Workbenchmake deploy<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0430\u0440\u043e\u043b\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0432\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044b \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0441\u0430\u043c\u0438.<\/p>\n<hr\/>\n<h3>\u0422\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0441\u0442\u0435\u043a \u0438 \u043f\u043e\u0447\u0435\u043c\u0443 \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u043d<\/h3>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">\u0421\u043b\u043e\u0439<\/p>\n<\/th>\n<th>\n<p align=\"left\">\u0418\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442<\/p>\n<\/th>\n<th>\n<p align=\"left\">\u0417\u0430\u0447\u0435\u043c<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0410\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0421\u0423\u0411\u0414<\/p>\n<\/td>\n<td>\n<p align=\"left\">ClickHouse 23.8<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u041a\u043e\u043b\u043e\u043d\u043e\u0447\u043d\u0430\u044f \u0411\u0414, \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0432 10\u2013100\u00d7 \u0431\u044b\u0441\u0442\u0440\u0435\u0435 PostgreSQL<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u041e\u0431\u044a\u0435\u043a\u0442\u043d\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435<\/p>\n<\/td>\n<td>\n<p align=\"left\">MinIO<\/p>\n<\/td>\n<td>\n<p align=\"left\">S3-\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u2014 \u0442\u043e\u0442 \u0436\u0435 \u043a\u043e\u0434 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 AWS<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u041e\u0447\u0435\u0440\u0435\u0434\u044c<\/p>\n<\/td>\n<td>\n<p align=\"left\">Apache Kafka<\/p>\n<\/td>\n<td>\n<p align=\"left\">At-least-once delivery, replay \u0434\u0430\u043d\u043d\u044b\u0445, \u0440\u0430\u0437\u0432\u044f\u0437\u043a\u0430 producer\/consumer<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Batch-\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430<\/p>\n<\/td>\n<td>\n<p align=\"left\">Apache Spark 3.5<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u0442\u044f\u0436\u0451\u043b\u044b\u0445 \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0439<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0422\u0440\u0430\u043d\u0441\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438<\/p>\n<\/td>\n<td>\n<p align=\"left\">dbt 1.7 + ClickHouse<\/p>\n<\/td>\n<td>\n<p align=\"left\">SQL \u0441 \u0442\u0435\u0441\u0442\u0430\u043c\u0438, lineage, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u041e\u0440\u043a\u0435\u0441\u0442\u0440\u0430\u0446\u0438\u044f<\/p>\n<\/td>\n<td>\n<p align=\"left\">Apache Airflow 2.8<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u0420\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435, \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433, retry<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">BI<\/p>\n<\/td>\n<td>\n<p align=\"left\">Apache Superset 3.0<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u0414\u0430\u0448\u0431\u043e\u0440\u0434\u044b<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0438\u0437\u0430\u0446\u0438\u044f<\/p>\n<\/td>\n<td>\n<p align=\"left\">Docker Compose v2<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u0412\u0435\u0441\u044c \u0441\u0442\u0435\u043a \u043e\u0434\u043d\u043e\u0439 \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p><strong>\u041f\u043e\u0447\u0435\u043c\u0443 ClickHouse, \u0430 \u043d\u0435 PostgreSQL \u0434\u043b\u044f \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0438?<\/strong><\/p>\n<p>PostgreSQL \u2014 \u0441\u0442\u0440\u043e\u0447\u043d\u0430\u044f \u0421\u0423\u0411\u0414. \u041d\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 <code>SELECT AVG(close) FROM klines GROUP BY symbol, date<\/code> \u043e\u043d \u0447\u0438\u0442\u0430\u0435\u0442 \u0432\u0441\u0435 \u043a\u043e\u043b\u043e\u043d\u043a\u0438 \u043a\u0430\u0436\u0434\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0438. ClickHouse \u0447\u0438\u0442\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u043b\u043e\u043d\u043a\u0438 <code>close<\/code>, <code>symbol<\/code>, <code>open_time<\/code> \u2014 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u0435\u0442. \u041d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 \u0432 200k \u0441\u0442\u0440\u043e\u043a \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043d\u0435\u0437\u0430\u043c\u0435\u0442\u043d\u0430. \u041d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 \u0432 200 \u043c\u043b\u043d \u2014 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0438\u0430\u043b\u044c\u043d\u0430.<\/p>\n<p><strong>\u041f\u043e\u0447\u0435\u043c\u0443 MinIO, \u0430 \u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u0438\u0441\u043a?<\/strong><\/p>\n<p>MinIO \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 S3 API. \u041a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0438\u0448\u0435\u0442 \u0432 MinIO, \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0437\u0430\u043f\u0438\u0448\u0435\u0442 \u0432 AWS S3 \u2014 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u043c\u0435\u043d\u044f\u0442\u044c endpoint \u0432 <code>.env<\/code>. \u042d\u0442\u043e \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0438\u0430\u043b\u044c\u043d\u043e \u0434\u043b\u044f production-ready \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b.<\/p>\n<p><strong>\u041f\u043e\u0447\u0435\u043c\u0443 dbt \u0418 Spark \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e?<\/strong><\/p>\n<p>\u042d\u0442\u043e \u043d\u0435 \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u0430 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438:<\/p>\n<ul>\n<li>\n<p><strong>dbt<\/strong> \u2014 SQL-\u0442\u0440\u0430\u043d\u0441\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0432 ClickHouse: ETL, \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438, \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445. \u041f\u0440\u043e\u0441\u0442\u043e, \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e, \u0431\u044b\u0441\u0442\u0440\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f.<\/p>\n<\/li>\n<li>\n<p><strong>Spark<\/strong> \u2014 \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u0447, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u0443\u044e\u0442\u0441\u044f: rolling window \u043f\u043e 90 \u0434\u043d\u044f\u043c \u043c\u0438\u043d\u0443\u0442\u043d\u044b\u0445 \u0441\u0432\u0435\u0447\u0435\u0439 \u043f\u043e \u0432\u0441\u0435\u043c \u0441\u0438\u043c\u0432\u043e\u043b\u0430\u043c. \u041f\u0440\u0438 \u0440\u043e\u0441\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0448\u044c workers, \u0432\u0440\u0435\u043c\u044f \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f.<\/p>\n<\/li>\n<\/ul>\n<hr\/>\n<h3>\u041c\u0435\u0434\u0430\u043b\u044c\u043e\u043d\u043d\u0430\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430: bronze, silver, gold<\/h3>\n<p>\u042d\u0442\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442 \u0438\u043d\u0434\u0443\u0441\u0442\u0440\u0438\u0438. \u0414\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u044f\u0442 \u0442\u0440\u0438 \u0443\u0440\u043e\u0432\u043d\u044f \u043e\u0447\u0438\u0441\u0442\u043a\u0438:<\/p>\n<pre><code>BRONZE (\u0441\u044b\u0440\u044b\u0435)  \u2192  SILVER (\u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435)  \u2192  GOLD (\u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>Bronze \u2014 \u043f\u0440\u0438\u043d\u0446\u0438\u043f \u043d\u0435\u0438\u0437\u043c\u0435\u043d\u043d\u043e\u0441\u0442\u0438<\/h4>\n<p>\u0414\u0430\u043d\u043d\u044b\u0435 \u043a\u0430\u043a \u043f\u0440\u0438\u0448\u043b\u0438 \u0441 \u0431\u0438\u0440\u0436\u0438, \u0442\u0430\u043a \u0438 \u043b\u0435\u0436\u0430\u0442. \u0412\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u043c\u0435\u0442\u043a\u0438 \u0432 \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, raw JSON-\u043f\u043e\u043b\u044f. <strong>\u041d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u044f\u0435\u043c, \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u043c.<\/strong><\/p>\n<pre><code class=\"sql\">CREATE TABLE bronze_klines (    ingested_at  DateTime DEFAULT now(),    exchange     LowCardinality(String),    symbol       LowCardinality(String),    open_time    Int64,        -- \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u044b, \u043a\u0430\u043a \u0432 API Binance    open         Float64,    ...    _source_file String        -- \u043f\u0443\u0442\u044c \u043a Parquet \u0432 MinIO) ENGINE = MergeTree()PARTITION BY toYYYYMM(_partition_date)ORDER BY (exchange, symbol, interval, open_time);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0417\u0430\u0447\u0435\u043c \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u00ab\u0441\u044b\u0440\u044c\u0451\u00bb? \u0415\u0441\u043b\u0438 \u0442\u0440\u0430\u043d\u0441\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u043a\u0430\u0437\u0430\u043b\u0430\u0441\u044c \u043d\u0435\u0432\u0435\u0440\u043d\u043e\u0439 \u2014 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0448\u044c \u0435\u0451 \u0441 \u043d\u0443\u043b\u044f. Bronze \u2014 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0440\u0430\u0432\u0434\u044b, \u0438\u0437 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u0435\u0441\u044c pipeline.<\/p>\n<h4>Silver \u2014 \u0438\u0434\u0435\u043c\u043f\u043e\u0442\u0435\u043d\u0442\u043d\u043e\u0441\u0442\u044c \u0447\u0435\u0440\u0435\u0437 ReplacingMergeTree<\/h4>\n<p>\u0417\u0434\u0435\u0441\u044c \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f: <code>Int64<\/code> \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u044b \u2192 <code>DateTime<\/code> UTC, \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0435 \u0442\u0438\u043f\u044b. \u041a\u043b\u044e\u0447\u0435\u0432\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u2014 \u0434\u0432\u0438\u0436\u043e\u043a \u0442\u0430\u0431\u043b\u0438\u0446\u044b:<\/p>\n<pre><code class=\"sql\">CREATE TABLE silver_klines (    exchange    LowCardinality(String),    symbol      LowCardinality(String),    open_time   DateTime,    -- \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0439 UTC    close       Float64,    ...) ENGINE = ReplacingMergeTree(ingested_at)ORDER BY (exchange, symbol, interval, open_time);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>ReplacingMergeTree(ingested_at)<\/code> \u2014 \u043f\u0440\u0438 \u043d\u0430\u043b\u0438\u0447\u0438\u0438 \u0434\u0443\u0431\u043b\u0435\u0439 \u043f\u043e \u043a\u043b\u044e\u0447\u0443 <code>(exchange, symbol, interval, open_time)<\/code> \u0434\u0432\u0438\u0436\u043e\u043a \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 \u0441 \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u043c <code>ingested_at<\/code>. \u042d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442: \u043f\u0430\u0439\u043f\u043b\u0430\u0439\u043d \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u0432\u0430\u0436\u0434\u044b \u2014 \u043b\u0438\u0448\u043d\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f. <code>SELECT ... FINAL<\/code> \u0444\u043e\u0440\u0441\u0438\u0440\u0443\u0435\u0442 \u0441\u043b\u0438\u044f\u043d\u0438\u0435 \u0434\u0443\u0431\u043b\u0435\u0439 \u043d\u0430 \u043b\u0435\u0442\u0443.<\/p>\n<h4>Gold \u2014 dbt \u0441\u0442\u0440\u043e\u0438\u0442 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0443<\/h4>\n<pre><code>silver_klines    \u2514\u2500\u2500 stg_klines (view, \u0442\u043e\u043d\u043a\u0430\u044f \u043e\u0431\u0451\u0440\u0442\u043a\u0430 \u0441 FINAL)            \u251c\u2500\u2500 fact_candles    (enriched: price_change_pct, is_bullish)            \u2514\u2500\u2500 mart_volatility (rolling vol 7d\/30d, ATR-14)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>dbt \u0441\u0442\u0440\u043e\u0438\u0442 \u0433\u0440\u0430\u0444 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u0437 <code>{{ ref('stg_klines') }}<\/code> \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043c\u043e\u0434\u0435\u043b\u0438 \u0432 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u043c \u043f\u043e\u0440\u044f\u0434\u043a\u0435. \u041f\u043b\u044e\u0441 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b:<\/p>\n<pre><code class=\"yaml\">- name: is_bullish  tests:    - accepted_values:        values: [0, 1]  # dbt \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 SQL-\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<hr\/>\n<h3>\u0421\u0442\u0440\u0438\u043c\u0438\u043d\u0433: Kafka + WebSocket<\/h3>\n<h4>\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c<\/h4>\n<p>\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u0441\u044f \u043a Binance combined stream \u2014 \u043e\u0434\u0438\u043d WebSocket \u043d\u0430 \u0432\u0441\u0435 \u0441\u0438\u043c\u0432\u043e\u043b\u044b:<\/p>\n<pre><code class=\"python\">uri = \"wss:\/\/stream.binance.com:9443\/stream?streams=\" + \\      \"\/\".join(f\"{s.lower()}@kline_1m\" for s in SYMBOLS)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u0430\u0436\u0434\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 Pydantic \u043f\u0435\u0440\u0435\u0434 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0435\u0439 \u0432 Kafka. \u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0443\u0445\u043e\u0434\u044f\u0442 \u0432 Dead Letter Queue, \u0430 \u043d\u0435 \u0442\u0435\u0440\u044f\u044e\u0442\u0441\u044f. Reconnect \u0441 exponential backoff: 1s, 2s, 4s\u2026 \u0434\u043e 60s.<\/p>\n<h4>\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044c<\/h4>\n<p>\u041a\u043b\u044e\u0447\u0435\u0432\u043e\u0439 \u043f\u0430\u0442\u0442\u0435\u0440\u043d \u2014 <strong>at-least-once delivery \u0441 \u0440\u0443\u0447\u043d\u044b\u043c commit<\/strong>:<\/p>\n<pre><code class=\"python\"># \u041d\u0430\u043a\u0430\u043f\u043b\u0438\u0432\u0430\u0435\u043c \u0431\u0430\u0442\u0447: 50 \u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0445 \u0441\u0432\u0435\u0447\u0435\u0439 \u0418\u041b\u0418 10 \u0441\u0435\u043a\u0443\u043d\u0434while True:    msg = consumer.poll(timeout=1.0)    if msg and msg.value().is_closed:        batch.append(msg)        if len(batch) &gt;= 50 or time_since_last_flush &gt; 10:        clickhouse.insert(batch)          # \u043f\u0438\u0448\u0435\u043c \u0432 ClickHouse        consumer.commit()                  # \u0422\u041e\u041b\u042c\u041a\u041e \u041f\u041e\u0421\u041b\u0415 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438        batch.clear()<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0415\u0441\u043b\u0438 ClickHouse \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u2014 offset \u043d\u0435 \u043a\u043e\u043c\u043c\u0438\u0442\u0438\u0442\u0441\u044f, \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u044b \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043f\u043e\u0441\u043b\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f.<\/p>\n<p>\u041f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e \u0441 \u0437\u0430\u043f\u0438\u0441\u044c\u044e \u0432 ClickHouse \u2014 \u0434\u0435\u0442\u0435\u043a\u0446\u0438\u044f \u0430\u043d\u043e\u043c\u0430\u043b\u0438\u0439 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438. \u0414\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0441\u0438\u043c\u0432\u043e\u043b\u0430 \u0434\u0435\u0440\u0436\u0438\u043c rolling window \u0438\u0437 60 \u0441\u0432\u0435\u0447\u0435\u0439:<\/p>\n<pre><code class=\"python\"># Volume spike: z-score \u043e\u0431\u044a\u0451\u043c\u0430 &gt; 2.5\u03c3z_score = (current_volume - mean_volume) \/ std_volumeif z_score &gt; 2.5:    emit_signal(\"volume_spike\", z_score)# Large candle: \u0440\u0430\u0437\u043c\u0430\u0445 &gt; 3\u00d7 ATR-14candle_range = abs(high - low)if candle_range &gt; 3.0 * atr_14:    emit_signal(\"large_candle\", candle_range \/ atr_14)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u0438\u0433\u043d\u0430\u043b\u044b \u043f\u0438\u0448\u0443\u0442\u0441\u044f \u0432 <code>rt_signals<\/code> \u0441 TTL 7 \u0434\u043d\u0435\u0439 \u2014 ClickHouse \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0443\u0434\u0430\u043b\u044f\u0435\u0442 \u0441\u0442\u0430\u0440\u044b\u0435.<\/p>\n<hr\/>\n<h3>Spark: \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435 \u0440\u044b\u043d\u043e\u0447\u043d\u044b\u0445 \u0440\u0435\u0436\u0438\u043c\u043e\u0432<\/h3>\n<p>\u042d\u0442\u043e \u0441\u0430\u043c\u0430\u044f \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u0441 \u0442\u043e\u0447\u043a\u0438 \u0437\u0440\u0435\u043d\u0438\u044f data engineering. \u0427\u0438\u0442\u0430\u0435\u043c <code>silver_klines<\/code> \u0447\u0435\u0440\u0435\u0437 JDBC, \u0430\u0433\u0440\u0435\u0433\u0438\u0440\u0443\u0435\u043c \u043f\u043e \u0434\u043d\u044f\u043c, \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 Spark window functions:<\/p>\n<pre><code class=\"python\"># Rolling realized volatility \u2014 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442 \u0432 quantitative financewindow_7d = Window.partitionBy(\"exchange\", \"symbol\") \\                  .orderBy(\"trade_date\") \\                  .rowsBetween(-6, 0)df = df.withColumn(    \"vol_7d\",    F.stddev_samp(\"log_return\").over(window_7d) * F.sqrt(F.lit(365))    # \u0430\u043d\u043d\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f: \u0443\u043c\u043d\u043e\u0436\u0430\u0435\u043c \u043d\u0430 \u221a365 \u0434\u043b\u044f calendar days volatility)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0440\u044b\u043d\u043e\u0447\u043d\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430:<\/p>\n<pre><code class=\"python\">regime = F.when(F.col(\"vol_7d\") &gt; F.lit(1.5) * F.col(\"vol_30d\"), \"volatile\") \\          .when((F.col(\"close\") - F.col(\"sma_20\")) \/ F.col(\"sma_20\") &gt; 0.02, \"trending_up\") \\          .when((F.col(\"sma_20\") - F.col(\"close\")) \/ F.col(\"sma_20\") &gt; 0.02, \"trending_down\") \\          .otherwise(\"ranging\")<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0432 <code>mart_market_regime<\/code> \u2014 \u043a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043d\u044c \u043f\u043e \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u0441\u0438\u043c\u0432\u043e\u043b\u0443 \u0438\u043c\u0435\u0435\u0442 \u043c\u0435\u0442\u043a\u0443 \u0440\u0435\u0436\u0438\u043c\u0430.<\/p>\n<p>\u041f\u043e\u0447\u0435\u043c\u0443 \u0438\u043c\u0435\u043d\u043d\u043e Spark, \u0430 \u043d\u0435 ClickHouse SQL? \u041f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u0440\u0438 \u0440\u043e\u0441\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 Spark \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u0443\u0435\u0442\u0441\u044f: \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c worker-\u043d\u043e\u0434\u044b \u0432 docker-compose \u0438 \u0432\u0440\u0435\u043c\u044f \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u043d\u0435 \u0440\u0430\u0441\u0442\u0451\u0442.<\/p>\n<hr\/>\n<h3>\u041e\u0440\u043a\u0435\u0441\u0442\u0440\u0430\u0446\u0438\u044f: Airflow \u0431\u0435\u0437 \u0440\u0443\u0447\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438<\/h3>\n<p>\u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0441 Airflow: \u043f\u043e\u0441\u043b\u0435 \u0434\u0435\u043f\u043b\u043e\u044f \u043d\u0443\u0436\u043d\u043e \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0441\u043e\u0437\u0434\u0430\u0442\u044c Connection \u0434\u043b\u044f Spark, \u0437\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c Variables \u0441 \u043f\u0430\u0440\u043e\u043b\u044f\u043c\u0438. \u042f \u0440\u0435\u0448\u0438\u043b \u044d\u0442\u043e \u0432 <code>airflow-init<\/code>:<\/p>\n<pre><code class=\"bash\"># \u0421\u043e\u0437\u0434\u0430\u0451\u043c Spark connection \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438airflow connections add spark_default \\    --conn-type spark \\    --conn-host \"spark:\/\/spark-master\" \\    --conn-port 7077 || true# \u0417\u0430\u043f\u043e\u043b\u043d\u044f\u0435\u043c Variables (\u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439 \u0435\u0441\u043b\u0438 DAG \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 var.value.get)airflow variables set CLICKHOUSE_PASSWORD \"${CLICKHOUSE_PASSWORD}\"<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u042d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432 <code>docker-compose.yml<\/code> \u0434\u043b\u044f <code>airflow-init<\/code> \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430. <code>|| true<\/code> \u2014 \u0435\u0441\u043b\u0438 connection \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 (redeploy), \u043d\u0435 \u043f\u0430\u0434\u0430\u0435\u043c \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439.<\/p>\n<p>DAGs \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 <code>os.environ.get()<\/code> \u0432\u043c\u0435\u0441\u0442\u043e Jinja <code>{{ var.value.get() }}<\/code>:<\/p>\n<pre><code class=\"python\"># \u041f\u043b\u043e\u0445\u043e: \u0447\u0438\u0442\u0430\u0435\u0442 \u0438\u0437 Airflow Variables (\u043d\u0443\u0436\u0435\u043d \u0440\u0443\u0447\u043d\u043e\u0439 \u0448\u0430\u0433 \u0438\u043b\u0438 Variables seeding)\"CLICKHOUSE_PASSWORD\": \"{{ var.value.get('CLICKHOUSE_PASSWORD', '') }}\"# \u0425\u043e\u0440\u043e\u0448\u043e: \u0447\u0438\u0442\u0430\u0435\u0442 \u0438\u0437 env \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u043f\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435 DAG\"CLICKHOUSE_PASSWORD\": os.environ.get(\"CLICKHOUSE_PASSWORD\", \"\")<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0420\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u0441 \u0443\u043c\u044b\u0441\u043b\u043e\u043c:<\/p>\n<pre><code>00:00, 06:00, 12:00, 18:00  \u2192  daily_pipeline (backfill + metadata + dbt)00:30, 06:30, 12:30, 18:30  \u2192  spark_batch (volatility + market regime)02:00 \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e             \u2192  data_quality (freshness + dbt tests + row counts)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>+30 \u043c\u0438\u043d\u0443\u0442 offset \u0443 Spark \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u0443\u0435\u0442 \u0447\u0442\u043e dbt \u0443\u0436\u0435 \u043f\u043e\u043b\u043e\u0436\u0438\u043b \u0441\u0432\u0435\u0436\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 silver_klines.<\/p>\n<hr\/>\n<h3>Superset: \u0434\u0430\u0448\u0431\u043e\u0440\u0434 \u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438<\/h3>\n<h4>\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430<\/h4>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0434\u0435\u043f\u043b\u043e\u044f Superset \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u043e \u043f\u0443\u0441\u0442\u043e\u0439. \u041d\u0443\u0436\u043d\u043e: \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Database connection, \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0430\u0441\u0435\u0442\u044b, \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0447\u0430\u0440\u0442\u044b, \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0434\u0430\u0448\u0431\u043e\u0440\u0434. \u042d\u0442\u043e 20+ \u043a\u043b\u0438\u043a\u043e\u0432 \u0432 UI \u2014 \u043d\u0435\u043f\u0440\u0438\u0435\u043c\u043b\u0435\u043c\u043e \u0434\u043b\u044f \u201c\u043e\u0434\u043d\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0434\u0435\u043f\u043b\u043e\u0439\u201d.<\/p>\n<h4>\u0420\u0435\u0448\u0435\u043d\u0438\u0435<\/h4>\n<p>\u0412\u0435\u0441\u044c \u044d\u0442\u043e\u0442 \u043a\u043e\u0434 \u0436\u0438\u0432\u0451\u0442 \u0432 <code>docker\/superset\/entrypoint.sh<\/code> \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u0441\u0442\u0430\u0440\u0442\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430:<\/p>\n<pre><code class=\"python\">app = create_app()with app.app_context():    # 1. \u041f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0451\u043c DB connection (\u043f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0433 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c\u0441\u044f \u043f\u0440\u0438 redeploy)    database = Database(        database_name=\"ClickHouse\",        sqlalchemy_uri=f\"clickhouse+http:\/\/{user}:{pw}@{host}:{port}\/{db}\"    )    # 2. \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043c \u0432\u0441\u0435 \u0434\u0430\u0442\u0430\u0441\u0435\u0442\u044b    for table_name in TABLES:        tbl = SqlaTable(table_name=table_name, database_id=database.id)        db.session.add(tbl)        tbl.fetch_metadata()  # \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u0441\u0445\u0435\u043c\u0443 \u043a\u043e\u043b\u043e\u043d\u043e\u043a    # 3. \u0421\u043e\u0437\u0434\u0430\u0451\u043c 6 \u0447\u0430\u0440\u0442\u043e\u0432 \u0438 \u0434\u0430\u0448\u0431\u043e\u0440\u0434    # (\u0432\u0435\u0440\u0441\u0438\u043e\u043d\u0438\u0440\u0443\u0435\u043c \u0447\u0435\u0440\u0435\u0437 json_metadata.init_version \u2014 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0451\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435 \u0432\u0435\u0440\u0441\u0438\u0438)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>\u0411\u0430\u0433 \u0441 \u0434\u0432\u043e\u0439\u043d\u044b\u043c time grain \u0432 ClickHouse<\/h4>\n<p>\u042d\u0442\u043e \u0437\u0430\u043d\u044f\u043b\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441\u043e\u0432 \u0434\u0435\u0431\u0430\u0433\u0430. Superset \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043b GROUP BY \u0441 \u0434\u0432\u043e\u0439\u043d\u044b\u043c \u043e\u0431\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u043d\u0438\u0435\u043c:<\/p>\n<pre><code class=\"sql\">-- \u041e\u0436\u0438\u0434\u0430\u043b\u043e\u0441\u044c:GROUP BY toStartOfDay(open_time), symbol-- \u041f\u043e\u043b\u0443\u0447\u0430\u043b\u043e\u0441\u044c (\u043e\u0448\u0438\u0431\u043a\u0430 ClickHouse NOT_AN_AGGREGATE):GROUP BY toStartOfDay(toDateTime(toStartOfDay(toDateTime(open_time)))), symbol<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0440\u0438\u0447\u0438\u043d\u0430: <code>clickhouse-sqlalchemy==0.2.5<\/code> \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442 grain-\u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043a \u0443\u0436\u0435 \u0430\u043b\u0438\u0430\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c\u0443 \u0441\u0442\u043e\u043b\u0431\u0446\u0443 \u0432 GROUP BY, \u0430 \u043d\u0435 \u043a \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u043c\u0443 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u044e. ClickHouse strict mode \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u044d\u0442\u043e \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u043c\u0438.<\/p>\n<p><strong>\u0420\u0435\u0448\u0435\u043d\u0438\u0435<\/strong>: \u0441\u043e\u0437\u0434\u0430\u0451\u043c pre-aggregated view \u0432 ClickHouse \u0438 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c <code>time_grain_sqla: None<\/code> \u0432 \u0447\u0430\u0440\u0442\u0430\u0445:<\/p>\n<pre><code class=\"python\"># \u0421\u043e\u0437\u0434\u0430\u0451\u043c view \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 Superset \u0447\u0435\u0440\u0435\u0437 HTTP API ClickHouse_ch_exec(\"\"\"    CREATE OR REPLACE VIEW crypto.v_daily_klines AS    SELECT exchange, symbol,           toDate(open_time)            AS trade_date,  -- \u0443\u0436\u0435 Date, \u043d\u0435 DateTime           argMin(open,  open_time)     AS day_open,           max(high)                    AS day_high,           min(low)                     AS day_low,           argMax(close, open_time)     AS day_close,           sum(volume)                  AS day_volume    FROM crypto.silver_klines    WHERE interval = '1m'    GROUP BY exchange, symbol, trade_date\"\"\")<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c Superset \u0434\u0435\u043b\u0430\u0435\u0442 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 GROUP BY \u0431\u0435\u0437 grain-\u0444\u0443\u043d\u043a\u0446\u0438\u0439:<\/p>\n<pre><code class=\"sql\">SELECT trade_date, symbol, AVG(day_close)FROM v_daily_klinesGROUP BY trade_date, symbol  -- \u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0439 ClickHouse GROUP BY<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>clickhouse-sqlalchemy vs clickhouse-connect<\/h4>\n<p>\u0415\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u043e\u0434\u0432\u043e\u0434\u043d\u044b\u0439 \u043a\u0430\u043c\u0435\u043d\u044c: Superset 3.x \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 SQLAlchemy 1.4. <code>clickhouse-connect &gt;= 0.7<\/code> \u0442\u0440\u0435\u0431\u0443\u0435\u0442 SQLAlchemy 2.0. \u041f\u0440\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 <code>clickhouse-connect==0.7+<\/code> Superset \u043f\u0430\u0434\u0430\u0435\u0442 \u043f\u0440\u0438 \u043b\u044e\u0431\u043e\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043a ClickHouse.<\/p>\n<p>\u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u0432 <code>docker\/superset\/Dockerfile<\/code>:<\/p>\n<pre><code># clickhouse-connect 0.7+ \u0442\u0440\u0435\u0431\u0443\u0435\u0442 SQLAlchemy 2.0, Superset 3.x \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 1.4RUN pip install --no-cache-dir \\    clickhouse-sqlalchemy==0.2.5 \\    clickhouse-connect==0.6.23   # \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0418 URI \u0432\u0438\u0434\u0430 <code>clickhouse+http:\/\/<\/code> \u0432\u043c\u0435\u0441\u0442\u043e <code>clickhouse+connect:\/\/<\/code>.<\/p>\n<hr\/>\n<h3>\u0414\u0435\u043f\u043b\u043e\u0439: \u043a\u0430\u043a \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0438\u0437\u043d\u0443\u0442\u0440\u0438<\/h3>\n<h4>\u0413\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u043a\u0440\u0435\u0442\u043e\u0432<\/h4>\n<p>\u041f\u0435\u0440\u0432\u043e\u0435, \u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 <code>make deploy<\/code> \u2014 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 <code>setup.sh<\/code>:<\/p>\n<pre><code class=\"bash\">gen_pass()   { openssl rand -hex 24; }    # 48 hex-\u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432, \u0431\u0435\u0437 \u0441\u043f\u0435\u0446\u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432gen_fernet() { python3 -c \"import base64, os;    print(base64.urlsafe_b64encode(os.urandom(32)).decode())\"; }CLICKHOUSE_PASSWORD=$(gen_pass)AIRFLOW_FERNET_KEY=$(gen_fernet)# ... \u0435\u0449\u0451 7 \u043f\u0430\u0440\u043e\u043b\u0435\u0439# sed \u0441 \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0435\u043c | \u2014 base64 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u043b\u044d\u0448\u0438, \u043e\u043d\u0438 \u0441\u043b\u043e\u043c\u0430\u044e\u0442 s\/...\/sed -i \"s|change_me_clickhouse_password|${CLICKHOUSE_PASSWORD}|g\" .env<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043f\u043b\u043e\u0439 \u2014 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u043e\u043b\u0438. <code>.env<\/code> \u0432 <code>.gitignore<\/code>. \u0412 git \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e <code>.env.example<\/code> \u0441 \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0430\u043c\u0438.<\/p>\n<h4>\u041f\u043e\u0440\u044f\u0434\u043e\u043a \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432<\/h4>\n<pre><code class=\"yaml\"># Webserver \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0441\u043b\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0433\u043e airflow-initairflow-webserver:  depends_on:    postgres:      condition: service_healthy                  # \u0436\u0434\u0451\u0442 pg_isready    airflow-init:      condition: service_completed_successfully   # \u0436\u0434\u0451\u0442 exit 0<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>service_completed_successfully<\/code> \u2014 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043d (\u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0435 init-\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b). <code>service_healthy<\/code> \u2014 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0437\u0430\u043f\u0443\u0449\u0435\u043d \u0438 \u043e\u0442\u0432\u0435\u0447\u0430\u0442\u044c \u043d\u0430 healthcheck.<\/p>\n<h4>Makefile: \u044f\u0432\u043d\u043e\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439<\/h4>\n<p>Makefile \u0436\u0434\u0451\u0442 ClickHouse \u0438 MinIO \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c backfill:<\/p>\n<pre><code>deploy:    @bash setup.sh    DOCKER_BUILDKIT=0 $(COMPOSE) up --build -d    # DOCKER_BUILDKIT=0 \u2014 \u043e\u0431\u0445\u043e\u0434\u0438\u0442 \u0431\u0430\u0433 \u0441 IPv6 DNS \u0432 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 Linux-\u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f\u0445    @echo \"Waiting for ClickHouse...\"    @until docker inspect workbench-clickhouse \\        --format='{{.State.Health.Status}}' | grep -q healthy; do sleep 2; done    @echo \"Waiting for MinIO buckets...\"    @until docker inspect workbench-minio-init \\        --format='{{.State.Status}}' | grep -qE 'exited'; do sleep 2; done    $(COMPOSE) run --rm app python ingestion\/historical\/klines_backfill.py    $(COMPOSE) run --rm app python ingestion\/metadata\/coingecko_dims.py \\        || echo \"[warn] CoinGecko unavailable \u2014 dim_coin will be empty\"    $(COMPOSE) run --rm dbt dbt deps &amp;&amp; dbt build    $(COMPOSE) exec spark-master spark-submit volatility_batch.py \\        || echo \"[warn] Spark failed \u2014 run 'make spark-volatility' manually\"<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>|| echo \"[warn]\"<\/code> \u2014 graceful degradation. \u0415\u0441\u043b\u0438 CoinGecko \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 Spark \u0443\u043f\u0430\u043b, \u0434\u0435\u043f\u043b\u043e\u0439 \u043d\u0435 \u043f\u0440\u0435\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f. \u041arit\u0438\u0447\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 (klines) \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u044b, \u0434\u0430\u0448\u0431\u043e\u0440\u0434 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.<\/p>\n<hr\/>\n<h3>\u0427\u0442\u043e \u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u0441\u043b\u043e\u0436\u043d\u0435\u0435, \u0447\u0435\u043c \u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c<\/h3>\n<p><strong>1. Stale URI \u0432 Superset volume<\/strong><\/p>\n<p>\u041f\u0440\u0438 redeploy \u0441 \u043d\u043e\u0432\u044b\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043c Superset \u0431\u0440\u0430\u043b URI \u0438\u0437 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e volume. <code>Database.sqlalchemy_uri<\/code> \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0447\u0435\u0440\u0435\u0437 Fernet \u2014 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0435 \u043f\u043e\u043c\u043e\u0433\u0430\u043b\u043e. \u0420\u0435\u0448\u0435\u043d\u0438\u0435: \u0432\u0441\u0435\u0433\u0434\u0430 \u0443\u0434\u0430\u043b\u044f\u0442\u044c \u0438 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u044c \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435:<\/p>\n<pre><code class=\"python\">existing = db.session.query(Database).filter_by(database_name=\"ClickHouse\").first()if existing:    db.session.delete(existing)    db.session.commit()# \u0421\u043e\u0437\u0434\u0430\u0451\u043c \u0437\u0430\u043d\u043e\u0432\u043e \u0441 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043cdatabase = Database(database_name=\"ClickHouse\", sqlalchemy_uri=uri)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><strong>2. \u041f\u0440\u0430\u0432\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0432 dbt\/dbt_packages<\/strong><\/p>\n<p><code>dbt deps<\/code> \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 Docker-\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u043e\u0442 root. \u0424\u0430\u0439\u043b\u044b <code>dbt_packages\/<\/code> \u0441\u043e\u0437\u0434\u0430\u044e\u0442\u0441\u044f \u0441 owner=root. \u041f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 <code>rm -rf<\/code> \u043e\u0442 \u043e\u0431\u044b\u0447\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u2014 Permission denied.<\/p>\n<p>\u0420\u0435\u0448\u0435\u043d\u0438\u0435: \u043f\u0440\u0438 \u043f\u043e\u043b\u043d\u043e\u0439 \u043f\u0435\u0440\u0435\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <code>sudo rm -rf<\/code>.<\/p>\n<p><strong>3. DOCKER_BUILDKIT=0<\/strong><\/p>\n<p>\u041d\u0430 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 Linux-\u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0445 BuildKit \u043f\u044b\u0442\u0430\u0435\u0442\u0441\u044f \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c <code>registry-1.docker.io<\/code> \u0447\u0435\u0440\u0435\u0437 IPv6, \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 NXDOMAIN, \u0438 \u0441\u0431\u043e\u0440\u043a\u0430 \u043f\u0430\u0434\u0430\u0435\u0442 \u043d\u0430 \u0448\u0430\u0433\u0435 <code>FROM<\/code>. \u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 BuildKit (<code>DOCKER_BUILDKIT=0<\/code>) \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043d\u0430 legacy builder \u0441 IPv4-only DNS.<\/p>\n<p><strong>4. Spark JDBC authentication<\/strong><\/p>\n<p>Spark-\u043c\u0430\u0441\u0442\u0435\u0440 \u043d\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u043b <code>CLICKHOUSE_PASSWORD<\/code> \u0438\u0437 <code>.env<\/code> \u2014 \u0432 docker-compose \u0434\u043b\u044f spark-\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432 \u043f\u0440\u043e\u043f\u0438\u0441\u044b\u0432\u0430\u043b\u0438\u0441\u044c \u0442\u043e\u043b\u044c\u043a\u043e <code>SPARK_*<\/code> \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435. <code>.env<\/code> \u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043b\u0441\u044f \u043a\u0430\u043a \u0444\u0430\u0439\u043b (<code>\/app\/.env<\/code>), \u043d\u043e Python \u0447\u0435\u0440\u0435\u0437 <code>os.getenv()<\/code> \u0447\u0438\u0442\u0430\u0435\u0442 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430, \u0430 \u043d\u0435 \u0444\u0430\u0439\u043b.<\/p>\n<p>\u0420\u0435\u0448\u0435\u043d\u0438\u0435: \u044f\u0432\u043d\u043e \u043f\u0440\u043e\u043a\u0438\u043d\u0443\u0442\u044c \u0432\u0441\u0435 <code>CLICKHOUSE_*<\/code> \u0432 environment spark-master \u0438 spark-worker.<\/p>\n<hr\/>\n<h3>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u043d\u044b\u0435 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u044b<\/h3>\n<p><strong>\u0418\u0434\u0435\u043c\u043f\u043e\u0442\u0435\u043d\u0442\u043d\u043e\u0441\u0442\u044c \u0432\u0435\u0437\u0434\u0435<\/strong>: <code>ReplacingMergeTree<\/code>, <code>dbt build<\/code> \u0441 atomic swap \u0447\u0435\u0440\u0435\u0437 <code>EXCHANGE TABLES<\/code>, <code>CREATE OR REPLACE VIEW<\/code>, \u0432\u0435\u0440\u0441\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u0430. \u041b\u044e\u0431\u043e\u0439 \u0448\u0430\u0433 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c.<\/p>\n<p><strong>Lambda Architecture<\/strong>: batch \u0438 streaming \u043f\u0438\u0448\u0443\u0442 \u0432 <code>silver_klines<\/code>. dbt \u0438 Spark \u0447\u0438\u0442\u0430\u044e\u0442 \u0438\u0437 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<p><strong>Infrastructure as Code<\/strong>: \u0432\u0435\u0441\u044c \u0441\u0442\u0435\u043a \u043e\u043f\u0438\u0441\u0430\u043d \u0434\u0435\u043a\u043b\u0430\u0440\u0430\u0442\u0438\u0432\u043d\u043e. \u041d\u043e\u0432\u044b\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043a\u043b\u043e\u043d\u0438\u0440\u0443\u0435\u0442 \u0440\u0435\u043f\u043e \u2014 \u0432\u0441\u0451 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.<\/p>\n<p><strong>Graceful degradation<\/strong>: \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0448\u0430\u0433\u0438 (\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0435, Spark) \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u044e\u0442 \u0434\u0435\u043f\u043b\u043e\u0439 \u043f\u0440\u0438 \u0441\u0431\u043e\u0435.<\/p>\n<hr\/>\n<h3>\u0414\u0430\u0448\u0431\u043e\u0440\u0434<\/h3>\n<p>\u041f\u043e\u0441\u043b\u0435 <code>make deploy<\/code> \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c <code>http:\/\/localhost:8088<\/code>:<\/p>\n<ul>\n<li>\n<p><strong>\u0420\u044f\u0434 1<\/strong>: Price History (\u0432\u0441\u0435 5 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432, \u0434\u043d\u0435\u0432\u043d\u044b\u0435 \u0441\u0432\u0435\u0447\u0438) + Volume History<\/p>\n<\/li>\n<li>\n<p><strong>\u0420\u044f\u0434 2<\/strong>: Realized Volatility 7d (BTC vs ETH vs SOL) + Market Regime \u043f\u043e \u0441\u0438\u043c\u0432\u043e\u043b\u0430\u043c<\/p>\n<\/li>\n<li>\n<p><strong>\u0420\u044f\u0434 3<\/strong>: Live Prices (\u0442\u0435\u043a\u0443\u0449\u0438\u0435 \u0446\u0435\u043d\u044b, \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043a\u0430\u0436\u0434\u0443\u044e \u043c\u0438\u043d\u0443\u0442\u0443) + Trading Signals<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u0441\u0451 \u044d\u0442\u043e \u0431\u0435\u0437 \u0435\u0434\u0438\u043d\u043e\u0433\u043e \u043a\u043b\u0438\u043a\u0430 \u2014 \u0442\u043e\u043b\u044c\u043a\u043e <code>make deploy<\/code>.<\/p>\n<hr\/>\n<h3>\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434<\/h3>\n<p>\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439: <a href=\"https:\/\/github.com\/andreivolokhovskii-coder\/Crypto-Research-Workbench\" rel=\"noopener noreferrer nofollow\"><strong>github.com\/andreivolokhovskii-coder\/Crypto-Research-Workbench<\/strong><\/a><\/p>\n<p>\u0427\u0442\u043e \u0435\u0441\u0442\u044c \u0432 \u0440\u0435\u043f\u043e:<\/p>\n<ul>\n<li>\n<p>\u041f\u043e\u043b\u043d\u044b\u0439 docker-compose \u0441\u0442\u0435\u043a (14 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432)<\/p>\n<\/li>\n<li>\n<p>Ingestion: REST backfill + WebSocket streaming + CoinGecko metadata<\/p>\n<\/li>\n<li>\n<p>dbt \u043c\u043e\u0434\u0435\u043b\u0438 \u0441 \u0442\u0435\u0441\u0442\u0430\u043c\u0438<\/p>\n<\/li>\n<li>\n<p>Spark job \u0434\u043b\u044f market regime classification<\/p>\n<\/li>\n<li>\n<p>Airflow DAGs \u0441 \u0430\u0432\u0442\u043e\u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u043c\u0438 connections \u0438 variables<\/p>\n<\/li>\n<li>\n<p>Superset \u0441 \u0430\u0432\u0442\u043e\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u043c \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u043e\u043c<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b \u0432 <code>ARCHITECTURE.md<\/code><\/p>\n<\/li>\n<\/ul>\n<hr\/>\n<h3>\u0427\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435<\/h3>\n<p>\u041f\u0440\u043e\u0435\u043a\u0442 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 development-\u0434\u0435\u043f\u043b\u043e\u044f. \u0414\u043b\u044f production \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f:<\/p>\n<ul>\n<li>\n<p><strong>Multi-node Kafka<\/strong> \u0441 replication_factor \u2265 2<\/p>\n<\/li>\n<li>\n<p><strong>CeleryExecutor<\/strong> \u0432 Airflow \u0432\u043c\u0435\u0441\u0442\u043e LocalExecutor<\/p>\n<\/li>\n<li>\n<p><strong>ClickHouse Keeper<\/strong> \u0432\u043c\u0435\u0441\u0442\u043e single-node \u0434\u043b\u044f \u0440\u0435\u043f\u043b\u0438\u043a\u0430\u0446\u0438\u0438<\/p>\n<\/li>\n<li>\n<p><strong>Centralized logging<\/strong> (Grafana Loki \u0438\u043b\u0438 ELK)<\/p>\n<\/li>\n<li>\n<p><strong>Secrets management<\/strong> (Vault, AWS Secrets Manager) \u0432\u043c\u0435\u0441\u0442\u043e <code>.env<\/code><\/p>\n<\/li>\n<\/ul>\n<p>\u0415\u0441\u043b\u0438 \u0441\u0442\u0430\u0442\u044c\u044f \u043e\u043a\u0430\u0436\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0435\u0437\u043d\u043e\u0439 \u2014 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0447\u0430\u0441\u0442\u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0443, \u043a\u0430\u043a \u043f\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u044d\u0442\u043e \u0432 production \u043d\u0430 Kubernetes.<\/p>\n<hr\/>\n<p><em>\u0422\u0435\u0433\u0438: data engineering, clickhouse, kafka, apache spark, dbt, airflow, superset, docker, python<\/em><\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1047304\/\">https:\/\/habr.com\/ru\/articles\/1047304\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u041a\u043e\u0433\u0434\u0430 \u044f \u0438\u0441\u043a\u0430\u043b \u0443\u0447\u0435\u0431\u043d\u044b\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u044b \u043f\u043e data engineering, \u043a\u0430\u0440\u0442\u0438\u043d\u0430 \u0431\u044b\u043b\u0430 \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e\u0439: \u043b\u0438\u0431\u043e \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b \u043d\u0430 \u0434\u0432\u0430 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0430 (\u00ab\u043f\u0438\u0448\u0435\u043c \u0432 Kafka, \u0447\u0438\u0442\u0430\u0435\u043c \u0432 Spark\u00bb), \u043b\u0438\u0431\u043e enterprise-\u0441\u0445\u0435\u043c\u0430 \u0431\u0435\u0437 \u0435\u0434\u0438\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u0447\u043a\u0438 \u043a\u043e\u0434\u0430. \u041c\u043d\u0435 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0447\u0435\u0433\u043e-\u0442\u043e \u0441\u0440\u0435\u0434\u043d\u0435\u0433\u043e \u2014 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0442\u0435\u043a, \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b, \u043d\u043e \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0432\u0441\u0451 \u043f\u043e\u0434\u043d\u0438\u043c\u0430\u0435\u0442\u0441\u044f \u043e\u0434\u043d\u043e\u0439 \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439 make deploy \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442: \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u0441\u0431\u043e\u0440\u0430 \u0438 \u0430\u043d\u0430\u043b\u0438\u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u043e\u0432\u0430\u043b\u044e\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u043e\u0436\u043d\u043e \u0441\u043a\u043b\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0430 \u043b\u044e\u0431\u043e\u0439 \u043c\u0430\u0448\u0438\u043d\u0435 \u0441 Docker. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043e\u0431 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435, \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0445 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0440\u0435\u0448\u0435\u043d\u0438\u044f\u0445 \u0438 \u0433\u0440\u0430\u0431\u043b\u044f\u0445, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0432\u0441\u0442\u0440\u0435\u0442\u0438\u043b\u0438\u0441\u044c \u043f\u043e \u043f\u0443\u0442\u0438.\u0427\u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u044e \u0441\u043d\u0430\u0447\u0430\u043b\u0430, \u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u0430:\u0421\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 30 \u0434\u043d\u0435\u0439 \u0438\u0441\u0442\u043e\u0440\u0438\u0438 \u043f\u043e 5 \u0441\u0438\u043c\u0432\u043e\u043b\u0430\u043c (BTC\/ETH\/SOL\/BNB\/XRP) \u0432 \u043c\u043e\u043c\u0435\u043d\u0442 \u0434\u0435\u043f\u043b\u043e\u044f\u0421\u0442\u0440\u0438\u043c\u0438\u0442 live-\u0434\u0430\u043d\u043d\u044b\u0435 \u0441 Binance \u0447\u0435\u0440\u0435\u0437 WebSocket \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0430\u043d\u043e\u043c\u0430\u043b\u0438\u0438: \u0432\u0441\u043f\u043b\u0435\u0441\u043a\u0438 \u043e\u0431\u044a\u0451\u043c\u0430 \u0438 \u0430\u043d\u043e\u043c\u0430\u043b\u044c\u043d\u043e \u043a\u0440\u0443\u043f\u043d\u044b\u0435 \u0441\u0432\u0435\u0447\u0438\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 rolling volatility \u0438 \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u0440\u044b\u043d\u043e\u0447\u043d\u044b\u0435 \u0440\u0435\u0436\u0438\u043c\u044b \u0447\u0435\u0440\u0435\u0437 Apache Spark\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u0442 \u0432\u0441\u0451 \u043d\u0430 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u043c \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u0435 \u2014 \u0442\u043e\u0436\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0418 \u0432\u0435\u0441\u044c \u044d\u0442\u043e\u0442 \u0441\u0442\u0435\u043a \u043f\u043e\u0434\u043d\u0438\u043c\u0430\u0435\u0442\u0441\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439:git clone https:\/\/github.com\/andreivolokhovskii-coder\/Crypto-Research-Workbench.gitcd Crypto-Research-Workbenchmake deploy\u041f\u0430\u0440\u043e\u043b\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0432\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044b \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0441\u0430\u043c\u0438.\u0422\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0441\u0442\u0435\u043a \u0438 \u043f\u043e\u0447\u0435\u043c\u0443 \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u043d\u0421\u043b\u043e\u0439\u0418\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0417\u0430\u0447\u0435\u043c\u0410\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0421\u0423\u0411\u0414ClickHouse 23.8\u041a\u043e\u043b\u043e\u043d\u043e\u0447\u043d\u0430\u044f \u0411\u0414, \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0432 10\u2013100\u00d7 \u0431\u044b\u0441\u0442\u0440\u0435\u0435 PostgreSQL\u041e\u0431\u044a\u0435\u043a\u0442\u043d\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435MinIOS3-\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u2014 \u0442\u043e\u0442 \u0436\u0435 \u043a\u043e\u0434 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 AWS\u041e\u0447\u0435\u0440\u0435\u0434\u044cApache KafkaAt-least-once delivery, replay \u0434\u0430\u043d\u043d\u044b\u0445, \u0440\u0430\u0437\u0432\u044f\u0437\u043a\u0430 producer\/consumerBatch-\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430Apache Spark 3.5\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u0442\u044f\u0436\u0451\u043b\u044b\u0445 \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0439\u0422\u0440\u0430\u043d\u0441\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438dbt 1.7 + ClickHouseSQL \u0441 \u0442\u0435\u0441\u0442\u0430\u043c\u0438, lineage, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439\u041e\u0440\u043a\u0435\u0441\u0442\u0440\u0430\u0446\u0438\u044fApache Airflow 2.8\u0420\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435, \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433, retryBIApache Superset 3.0\u0414\u0430\u0448\u0431\u043e\u0440\u0434\u044b\u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0438\u0437\u0430\u0446\u0438\u044fDocker Compose v2\u0412\u0435\u0441\u044c \u0441\u0442\u0435\u043a \u043e\u0434\u043d\u043e\u0439 \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439\u041f\u043e\u0447\u0435\u043c\u0443 ClickHouse, \u0430 \u043d\u0435 PostgreSQL \u0434\u043b\u044f \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0438?PostgreSQL \u2014 \u0441\u0442\u0440\u043e\u0447\u043d\u0430\u044f \u0421\u0423\u0411\u0414. \u041d\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 SELECT AVG(close) FROM klines GROUP BY symbol, date \u043e\u043d \u0447\u0438\u0442\u0430\u0435\u0442 \u0432\u0441\u0435 \u043a\u043e\u043b\u043e\u043d\u043a\u0438 \u043a\u0430\u0436\u0434\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0438. ClickHouse \u0447\u0438\u0442\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u043b\u043e\u043d\u043a\u0438 close, symbol, open_time \u2014 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u0435\u0442. \u041d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 \u0432 200k \u0441\u0442\u0440\u043e\u043a \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043d\u0435\u0437\u0430\u043c\u0435\u0442\u043d\u0430. \u041d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 \u0432 200 \u043c\u043b\u043d \u2014 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0438\u0430\u043b\u044c\u043d\u0430.\u041f\u043e\u0447\u0435\u043c\u0443 MinIO, \u0430 \u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u0438\u0441\u043a?MinIO \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 S3 API. \u041a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0438\u0448\u0435\u0442 \u0432 MinIO, \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0437\u0430\u043f\u0438\u0448\u0435\u0442 \u0432 AWS S3 \u2014 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u043c\u0435\u043d\u044f\u0442\u044c endpoint \u0432 .env. \u042d\u0442\u043e \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0438\u0430\u043b\u044c\u043d\u043e \u0434\u043b\u044f production-ready \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b.\u041f\u043e\u0447\u0435\u043c\u0443 dbt \u0418 Spark \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e?\u042d\u0442\u043e \u043d\u0435 \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u0430 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438:dbt \u2014 SQL-\u0442\u0440\u0430\u043d\u0441\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0432 ClickHouse: ETL, \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438, \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445. \u041f\u0440\u043e\u0441\u0442\u043e, \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e, \u0431\u044b\u0441\u0442\u0440\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f.Spark \u2014 \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u0447, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u0443\u044e\u0442\u0441\u044f: rolling window \u043f\u043e 90 \u0434\u043d\u044f\u043c \u043c\u0438\u043d\u0443\u0442\u043d\u044b\u0445 \u0441\u0432\u0435\u0447\u0435\u0439 \u043f\u043e \u0432\u0441\u0435\u043c \u0441\u0438\u043c\u0432\u043e\u043b\u0430\u043c. \u041f\u0440\u0438 \u0440\u043e\u0441\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0448\u044c workers, \u0432\u0440\u0435\u043c\u044f \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f.\u041c\u0435\u0434\u0430\u043b\u044c\u043e\u043d\u043d\u0430\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430: bronze, silver, gold\u042d\u0442\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442 \u0438\u043d\u0434\u0443\u0441\u0442\u0440\u0438\u0438. \u0414\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u044f\u0442 \u0442\u0440\u0438 \u0443\u0440\u043e\u0432\u043d\u044f \u043e\u0447\u0438\u0441\u0442\u043a\u0438:BRONZE (\u0441\u044b\u0440\u044b\u0435)  \u2192  SILVER (\u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435)  \u2192  GOLD (\u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435)Bronze \u2014 \u043f\u0440\u0438\u043d\u0446\u0438\u043f \u043d\u0435\u0438\u0437\u043c\u0435\u043d\u043d\u043e\u0441\u0442\u0438\u0414\u0430\u043d\u043d\u044b\u0435 \u043a\u0430\u043a \u043f\u0440\u0438\u0448\u043b\u0438 \u0441 \u0431\u0438\u0440\u0436\u0438, \u0442\u0430\u043a \u0438 \u043b\u0435\u0436\u0430\u0442. \u0412\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u043c\u0435\u0442\u043a\u0438 \u0432 \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, raw JSON-\u043f\u043e\u043b\u044f. \u041d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u044f\u0435\u043c, \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u043c.CREATE TABLE bronze_klines (    ingested_at  DateTime DEFAULT now(),    exchange     LowCardinality(String),    symbol       LowCardinality(String),    open_time    Int64,        &#8212; \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u044b, \u043a\u0430\u043a \u0432 API Binance    open         Float64,    &#8230;    _source_file String        &#8212; \u043f\u0443\u0442\u044c \u043a Parquet \u0432 MinIO) ENGINE = MergeTree()PARTITION BY toYYYYMM(_partition_date)ORDER BY (exchange, symbol, interval, open_time);\u0417\u0430\u0447\u0435\u043c \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u00ab\u0441\u044b\u0440\u044c\u0451\u00bb? \u0415\u0441\u043b\u0438 \u0442\u0440\u0430\u043d\u0441\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u043a\u0430\u0437\u0430\u043b\u0430\u0441\u044c \u043d\u0435\u0432\u0435\u0440\u043d\u043e\u0439 \u2014 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0448\u044c \u0435\u0451 \u0441 \u043d\u0443\u043b\u044f. Bronze \u2014 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0440\u0430\u0432\u0434\u044b, \u0438\u0437 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u0435\u0441\u044c pipeline.Silver \u2014 \u0438\u0434\u0435\u043c\u043f\u043e\u0442\u0435\u043d\u0442\u043d\u043e\u0441\u0442\u044c \u0447\u0435\u0440\u0435\u0437 ReplacingMergeTree\u0417\u0434\u0435\u0441\u044c \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f: Int64 \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u044b \u2192 DateTime UTC, \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0435 \u0442\u0438\u043f\u044b. \u041a\u043b\u044e\u0447\u0435\u0432\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u2014 \u0434\u0432\u0438\u0436\u043e\u043a \u0442\u0430\u0431\u043b\u0438\u0446\u044b:CREATE TABLE silver_klines (    exchange    LowCardinality(String),    symbol      LowCardinality(String),    open_time   DateTime,    &#8212; \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0439 UTC    close       Float64,    &#8230;) ENGINE = ReplacingMergeTree(ingested_at)ORDER BY (exchange, symbol, interval, open_time);ReplacingMergeTree(ingested_at) \u2014 \u043f\u0440\u0438 \u043d\u0430\u043b\u0438\u0447\u0438\u0438 \u0434\u0443\u0431\u043b\u0435\u0439 \u043f\u043e \u043a\u043b\u044e\u0447\u0443 (exchange, symbol, interval, open_time) \u0434\u0432\u0438\u0436\u043e\u043a \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 \u0441 \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u043c ingested_at. \u042d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442: \u043f\u0430\u0439\u043f\u043b\u0430\u0439\u043d \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u0432\u0430\u0436\u0434\u044b \u2014 \u043b\u0438\u0448\u043d\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f. SELECT &#8230; FINAL \u0444\u043e\u0440\u0441\u0438\u0440\u0443\u0435\u0442 \u0441\u043b\u0438\u044f\u043d\u0438\u0435 \u0434\u0443\u0431\u043b\u0435\u0439 \u043d\u0430 \u043b\u0435\u0442\u0443.Gold \u2014 dbt \u0441\u0442\u0440\u043e\u0438\u0442 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0443silver_klines    \u2514\u2500\u2500 stg_klines (view, \u0442\u043e\u043d\u043a\u0430\u044f \u043e\u0431\u0451\u0440\u0442\u043a\u0430 \u0441 FINAL)            \u251c\u2500\u2500 fact_candles    (enriched: price_change_pct, is_bullish)            \u2514\u2500\u2500 mart_volatility (rolling vol 7d\/30d, ATR-14)dbt \u0441\u0442\u0440\u043e\u0438\u0442 \u0433\u0440\u0430\u0444 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u0437 {{ ref(&#8216;stg_klines&#8217;) }} \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043c\u043e\u0434\u0435\u043b\u0438 \u0432 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u043c \u043f\u043e\u0440\u044f\u0434\u043a\u0435. \u041f\u043b\u044e\u0441 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b:- name: is_bullish  tests:    &#8212; accepted_values:        values: [0, 1]  # dbt \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 SQL-\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0421\u0442\u0440\u0438\u043c\u0438\u043d\u0433: Kafka + WebSocket\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u0441\u044f \u043a Binance combined stream \u2014 \u043e\u0434\u0438\u043d WebSocket \u043d\u0430 \u0432\u0441\u0435 \u0441\u0438\u043c\u0432\u043e\u043b\u044b:uri = &#171;wss:\/\/stream.binance.com:9443\/stream?streams=&#187; + \\      &#171;\/&#187;.join(f&#187;{s.lower()}@kline_1m&#187; for s in SYMBOLS)\u041a\u0430\u0436\u0434\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 Pydantic \u043f\u0435\u0440\u0435\u0434 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0435\u0439 \u0432 Kafka. \u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0443\u0445\u043e\u0434\u044f\u0442 \u0432 Dead Letter Queue, \u0430 \u043d\u0435 \u0442\u0435\u0440\u044f\u044e\u0442\u0441\u044f. Reconnect \u0441 exponential backoff: 1s, 2s, 4s\u2026 \u0434\u043e 60s.\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044c\u041a\u043b\u044e\u0447\u0435\u0432\u043e\u0439 \u043f\u0430\u0442\u0442\u0435\u0440\u043d \u2014 at-least-once delivery \u0441 \u0440\u0443\u0447\u043d\u044b\u043c commit:# \u041d\u0430\u043a\u0430\u043f\u043b\u0438\u0432\u0430\u0435\u043c \u0431\u0430\u0442\u0447: 50 \u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0445 \u0441\u0432\u0435\u0447\u0435\u0439 \u0418\u041b\u0418 10 \u0441\u0435\u043a\u0443\u043d\u0434while True:    msg = consumer.poll(timeout=1.0)    if msg and msg.value().is_closed:        batch.append(msg)        if len(batch) &gt;= 50 or time_since_last_flush &gt; 10:        clickhouse.insert(batch)          # \u043f\u0438\u0448\u0435\u043c \u0432 ClickHouse        consumer.commit()                  # \u0422\u041e\u041b\u042c\u041a\u041e \u041f\u041e\u0421\u041b\u0415 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438        batch.clear()\u0415\u0441\u043b\u0438 ClickHouse \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u2014 offset \u043d\u0435 \u043a\u043e\u043c\u043c\u0438\u0442\u0438\u0442\u0441\u044f, \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u044b \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043f\u043e\u0441\u043b\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f.\u041f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e \u0441 \u0437\u0430\u043f\u0438\u0441\u044c\u044e \u0432 ClickHouse \u2014 \u0434\u0435\u0442\u0435\u043a\u0446\u0438\u044f \u0430\u043d\u043e\u043c\u0430\u043b\u0438\u0439 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438. \u0414\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0441\u0438\u043c\u0432\u043e\u043b\u0430 \u0434\u0435\u0440\u0436\u0438\u043c rolling window \u0438\u0437 60 \u0441\u0432\u0435\u0447\u0435\u0439:# Volume spike: z-score \u043e\u0431\u044a\u0451\u043c\u0430 &gt; 2.5\u03c3z_score = (current_volume &#8212; mean_volume) \/ std_volumeif z_score &gt; 2.5:    emit_signal(&#171;volume_spike&#187;, z_score)# Large candle: \u0440\u0430\u0437\u043c\u0430\u0445 &gt; 3\u00d7 ATR-14candle_range = abs(high &#8212; low)if candle_range &gt; 3.0 * atr_14:    emit_signal(&#171;large_candle&#187;, candle_range \/ atr_14)\u0421\u0438\u0433\u043d\u0430\u043b\u044b \u043f\u0438\u0448\u0443\u0442\u0441\u044f \u0432 rt_signals \u0441 TTL 7 \u0434\u043d\u0435\u0439 \u2014 ClickHouse \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0443\u0434\u0430\u043b\u044f\u0435\u0442 \u0441\u0442\u0430\u0440\u044b\u0435.Spark: \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435 \u0440\u044b\u043d\u043e\u0447\u043d\u044b\u0445 \u0440\u0435\u0436\u0438\u043c\u043e\u0432\u042d\u0442\u043e \u0441\u0430\u043c\u0430\u044f \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u0441 \u0442\u043e\u0447\u043a\u0438 \u0437\u0440\u0435\u043d\u0438\u044f data engineering. \u0427\u0438\u0442\u0430\u0435\u043c silver_klines \u0447\u0435\u0440\u0435\u0437 JDBC, \u0430\u0433\u0440\u0435\u0433\u0438\u0440\u0443\u0435\u043c \u043f\u043e \u0434\u043d\u044f\u043c, \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 Spark window functions:# Rolling realized volatility \u2014 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442 \u0432 quantitative financewindow_7d = Window.partitionBy(&#171;exchange&#187;, &#171;symbol&#187;) \\                  .orderBy(&#171;trade_date&#187;) \\                  .rowsBetween(-6, 0)df = df.withColumn(    &#171;vol_7d&#187;,    F.stddev_samp(&#171;log_return&#187;).over(window_7d) * F.sqrt(F.lit(365))    # \u0430\u043d\u043d\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f: \u0443\u043c\u043d\u043e\u0436\u0430\u0435\u043c \u043d\u0430 \u221a365 \u0434\u043b\u044f calendar days volatility)\u041a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0440\u044b\u043d\u043e\u0447\u043d\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430:regime = F.when(F.col(&#171;vol_7d&#187;) &gt; F.lit(1.5) * F.col(&#171;vol_30d&#187;), &#171;volatile&#187;) \\          .when((F.col(&#171;close&#187;) &#8212; F.col(&#171;sma_20&#187;)) \/ F.col(&#171;sma_20&#187;) &gt; 0.02, &#171;trending_up&#187;) \\          .when((F.col(&#171;sma_20&#187;) &#8212; F.col(&#171;close&#187;)) \/ F.col(&#171;sma_20&#187;) &gt; 0.02, &#171;trending_down&#187;) \\          .otherwise(&#171;ranging&#187;)\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0432 mart_market_regime \u2014 \u043a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043d\u044c \u043f\u043e \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u0441\u0438\u043c\u0432\u043e\u043b\u0443 \u0438\u043c\u0435\u0435\u0442 \u043c\u0435\u0442\u043a\u0443 \u0440\u0435\u0436\u0438\u043c\u0430.\u041f\u043e\u0447\u0435\u043c\u0443 \u0438\u043c\u0435\u043d\u043d\u043e Spark, \u0430 \u043d\u0435 ClickHouse SQL? \u041f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u0440\u0438 \u0440\u043e\u0441\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 Spark \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u0443\u0435\u0442\u0441\u044f: \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c worker-\u043d\u043e\u0434\u044b \u0432 docker-compose \u0438 \u0432\u0440\u0435\u043c\u044f \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u043d\u0435 \u0440\u0430\u0441\u0442\u0451\u0442.\u041e\u0440\u043a\u0435\u0441\u0442\u0440\u0430\u0446\u0438\u044f: Airflow \u0431\u0435\u0437 \u0440\u0443\u0447\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0441 Airflow: \u043f\u043e\u0441\u043b\u0435 \u0434\u0435\u043f\u043b\u043e\u044f \u043d\u0443\u0436\u043d\u043e \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0441\u043e\u0437\u0434\u0430\u0442\u044c Connection \u0434\u043b\u044f Spark, \u0437\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c Variables \u0441 \u043f\u0430\u0440\u043e\u043b\u044f\u043c\u0438. \u042f \u0440\u0435\u0448\u0438\u043b \u044d\u0442\u043e \u0432 airflow-init:# \u0421\u043e\u0437\u0434\u0430\u0451\u043c Spark connection \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438airflow connections add spark_default \\    &#8212;conn-type spark \\    &#8212;conn-host &#171;spark:\/\/spark-master&#187; \\    &#8212;conn-port 7077 || true# \u0417\u0430\u043f\u043e\u043b\u043d\u044f\u0435\u043c Variables (\u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439 \u0435\u0441\u043b\u0438 DAG \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 var.value.get)airflow variables set CLICKHOUSE_PASSWORD &#171;${CLICKHOUSE_PASSWORD}&#187;\u042d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432 docker-compose.yml \u0434\u043b\u044f airflow-init \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430. || true \u2014 \u0435\u0441\u043b\u0438 connection \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 (redeploy), \u043d\u0435 \u043f\u0430\u0434\u0430\u0435\u043c \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439.DAGs \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 os.environ.get() \u0432\u043c\u0435\u0441\u0442\u043e Jinja {{ var.value.get() }}:# \u041f\u043b\u043e\u0445\u043e: \u0447\u0438\u0442\u0430\u0435\u0442 \u0438\u0437 Airflow Variables (\u043d\u0443\u0436\u0435\u043d \u0440\u0443\u0447\u043d\u043e\u0439 \u0448\u0430\u0433 \u0438\u043b\u0438 Variables seeding)&#187;CLICKHOUSE_PASSWORD&#187;: &#171;{{ var.value.get(&#8216;CLICKHOUSE_PASSWORD&#8217;, &#187;) }}&#187;# \u0425\u043e\u0440\u043e\u0448\u043e: \u0447\u0438\u0442\u0430\u0435\u0442 \u0438\u0437 env \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u043f\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0435 DAG&#187;CLICKHOUSE_PASSWORD&#187;: os.environ.get(&#171;CLICKHOUSE_PASSWORD&#187;, &#171;&#187;)\u0420\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u0441 \u0443\u043c\u044b\u0441\u043b\u043e\u043c:00:00, 06:00, 12:00, 18:00  \u2192  daily_pipeline (backfill + metadata + dbt)00:30, 06:30, 12:30, 18:30  \u2192  spark_batch (volatility + market regime)02:00 \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e             \u2192  data_quality (freshness + dbt tests + row counts)+30 \u043c\u0438\u043d\u0443\u0442 offset \u0443 Spark \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u0443\u0435\u0442 \u0447\u0442\u043e dbt \u0443\u0436\u0435 \u043f\u043e\u043b\u043e\u0436\u0438\u043b \u0441\u0432\u0435\u0436\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 silver_klines.Superset: \u0434\u0430\u0448\u0431\u043e\u0440\u0434 \u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u041f\u043e\u0441\u043b\u0435 \u0434\u0435\u043f\u043b\u043e\u044f Superset \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u043e \u043f\u0443\u0441\u0442\u043e\u0439. \u041d\u0443\u0436\u043d\u043e: \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Database connection, \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0430\u0441\u0435\u0442\u044b, \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0447\u0430\u0440\u0442\u044b, \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0434\u0430\u0448\u0431\u043e\u0440\u0434. \u042d\u0442\u043e 20+ \u043a\u043b\u0438\u043a\u043e\u0432 \u0432 UI \u2014 \u043d\u0435\u043f\u0440\u0438\u0435\u043c\u043b\u0435\u043c\u043e \u0434\u043b\u044f \u201c\u043e\u0434\u043d\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0434\u0435\u043f\u043b\u043e\u0439\u201d.\u0420\u0435\u0448\u0435\u043d\u0438\u0435\u0412\u0435\u0441\u044c \u044d\u0442\u043e\u0442 \u043a\u043e\u0434 \u0436\u0438\u0432\u0451\u0442 \u0432 docker\/superset\/entrypoint.sh \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u0441\u0442\u0430\u0440\u0442\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430:app = create_app()with app.app_context():    # 1. \u041f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0451\u043c DB connection (\u043f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0433 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c\u0441\u044f \u043f\u0440\u0438 redeploy)    database = Database(        database_name=&#187;ClickHouse&#187;,        sqlalchemy_uri=f&#187;clickhouse+http:\/\/{user}:{pw}@{host}:{port}\/{db}&#187;    )    # 2. \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043c \u0432\u0441\u0435 \u0434\u0430\u0442\u0430\u0441\u0435\u0442\u044b    for table_name in TABLES:        tbl = SqlaTable(table_name=table_name, database_id=database.id)        db.session.add(tbl)        tbl.fetch_metadata()  # \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u0441\u0445\u0435\u043c\u0443 \u043a\u043e\u043b\u043e\u043d\u043e\u043a    # 3. \u0421\u043e\u0437\u0434\u0430\u0451\u043c 6 \u0447\u0430\u0440\u0442\u043e\u0432 \u0438 \u0434\u0430\u0448\u0431\u043e\u0440\u0434    # (\u0432\u0435\u0440\u0441\u0438\u043e\u043d\u0438\u0440\u0443\u0435\u043c \u0447\u0435\u0440\u0435\u0437 json_metadata.init_version \u2014 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0451\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435 \u0432\u0435\u0440\u0441\u0438\u0438)\u0411\u0430\u0433 \u0441 \u0434\u0432\u043e\u0439\u043d\u044b\u043c time grain \u0432 ClickHouse\u042d\u0442\u043e \u0437\u0430\u043d\u044f\u043b\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441\u043e\u0432 \u0434\u0435\u0431\u0430\u0433\u0430. Superset \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043b GROUP BY \u0441 \u0434\u0432\u043e\u0439\u043d\u044b\u043c \u043e\u0431\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u043d\u0438\u0435\u043c:&#8212; \u041e\u0436\u0438\u0434\u0430\u043b\u043e\u0441\u044c:GROUP BY toStartOfDay(open_time), symbol&#8212; \u041f\u043e\u043b\u0443\u0447\u0430\u043b\u043e\u0441\u044c (\u043e\u0448\u0438\u0431\u043a\u0430 ClickHouse NOT_AN_AGGREGATE):GROUP BY&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-483560","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/483560","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=483560"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/483560\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=483560"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=483560"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=483560"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}