Полный реверс терминала: архитектура, все виджеты, чарт-движок, Indie DSL, рисование, стакан, трейдинг, данные, авторизация, сохранение и дизайн-система — с verbatim-кодом и живыми скринами.
TakeProfit — браузерный торговый терминал на единой WebGL-сцене. Ни один кусок не «обычный сайт»: график, стакан и пузыри — это GPU/Canvas-движки, а данные идут бинарным Protobuf через воркеры.
Главный приём производительности: весь workspace рисуется в один WebGL2-canvas на весь вьюпорт (как игровой движок), поверх — DOM-оверлеи виджетов (<section class="widget">) в докинг-сетке Lumino. Рендер on-demand (0 draw-call в покое).
Проверено по полному графу: весь код — 352 чанка / 12 МБ (замыкание импортов завершено). Во всём графе 0 ссылок на .wasm и нет WebAssembly.instantiate для расчётов → клиентского WASM нет; «WASM на C/C++» из маркетинга — серверный (рантайм Indie R_WASM) либо преувеличен.
Рабочее пространство — мозаика виджетов на Lumino DockPanel (быв. PhosphorJS; подтверждено CSS lm-DockPanel-*, MIME application/vnd.lumino.widget-factory, GOLDEN_RATIO=.618, BoxEngine.adjust). Поверх — кастомный LayoutManager TakeProfit.
split-area (orientation, sizes — относительные доли, children) и tab-area (widgets[{ID}], currentIndex). Рекурсивные сплиты + табы.findDropTarget): у краёв — root-зоны по порогам DEFAULT_EDGES={top:12,right:40,bottom:40,left:40}px; внутри виджета центральная треть → вкладка (widget-all), иначе ближайшая грань (left/top/right/bottom).overlay.hide(100) (100 мс).moveHandle+BoxEngine.adjust): zero-sum между соседями с учётом min/max — рост слева компенсируется сжатием справа. Дефолтный spacing=4px.1/n, иначе доля; новый сплит даёт первому ребёнку sizeHint=1, остальным 0.618.DRAG_THRESHOLD=5px, отрыв (tear-off) за пределами contentRect±DETACH_THRESHOLD=20px.createFakeBottom/Left/RightSideEvent — программный дроп к краям без курсора; helper avoid-zone «прилипает» к нужной стороне.DEFAULT_EDGES={top:12,right:40,bottom:40,left:40}Px-пороги root-зон drop.
case"root-top":...l=h.height*F.GOLDEN_RATIO;break;Оверлей root-top = 0.618 высоты (GOLDEN_RATIO=.618).
moveHandle(e,t){...for(let r of this._sizers)r.size>0&&(r.sizeHint=r.size);j.adjust(this._sizers,e,n),this.parent&&this.parent.update()}Сплиттер: фиксация sizeHint + zero-sum adjust.
s.DRAG_THRESHOLD=5,s.DETACH_THRESHOLD=20TabBar: старт drag при 5px, отрыв за contentRect±20px.
Хаб «Add Widgets» (правый drawer с превью-карточками и «+»). Полный список типов виджетов, найденный в коде и хабе:
| Виджет | Логика / источник данных |
|---|---|
| Chart | График: Lightweight Charts-модель + Pixi/WebGL2-рендер. Свечи/бары/линия/area/baseline, индикаторы в пейнах, drawing tools, crosshair, объёмы. См. §04–05. |
| Watchlist | Списки инструментов с live-котировками (QuoteApi/QuoteStream). Цена, изменение, %, биржа. |
| Screener | Скринер акций: StockScreenerApi/Search + CategoryApi, фильтры, сортировка по капитализации/метрикам. |
| Financials | Фундаментал: FundamentalApi (P/E, P/S, P/B, EV/EBITDA, dividend yield…), периоды Yr/QTR. |
| Market Depth | Стакан (DOM) + Order Flow Bubbles на Canvas2D (движок «TxOrderBook»). См. §08. |
| Indie® Code Editor | IDE на CodeMirror 6 для DSL Indie: индикаторы и стратегии, серверная компиляция. См. §06. |
| Strategy / Backtesting | Бэктест стратегий Indie: equity/drawdown, отчёт через StrategyReportStylistApi, диапазон 5000/20000 свечей. |
| Feed | Лента комьюнити прямо в workspace. |
| Notes | Заметки/доки с блочным редактором (drag блоков, поиск anchor бинарным поиском). |
| Account | Профиль, подписки, ключи бирж (TradingApi/StoreApiKey). |
| Lime / J2T / Ilotcos | Брокерские order/portfolio-виджеты — сторонние iframe (бэкенд Finam; J2T через MT5), мост postMessage. См. §09. |
Главное открытие: график — форк TradingView Lightweight Charts (scene-модель, оси, API addCandlestickSeries/coordinateToPrice, crosshair) с рендером, заменённым на кастомный PixiJS v7 / WebGL2. Каждый тип серии — свой GLSL-шейдер; цена/лог/процент считаются прямо в вершинном шейдере. Это и есть «первые WebGL-графики».
instanceCount=баров. Полный инстанс = 17 float (68 байт): time + OHLC + body/border/wick цвета; упрощённый = 9 float. Тело/границы/фитиль — во фрагментном шейдере, субпиксельный AA через subpixelLine(). Ширина uWidth=.6.uDrawOpen/uDrawBar).polyline: расширение сегментов (aPoint1/aPoint2) в толстую линию; area — заливка до базовой.uidColor (picking).SCALE_NORMAL/LOG/PERCENT/INDEXED. Лог: toLog(p)=0.4342944819·log(|p|+off)+… (0.434=log10 e). Процент: 100·(p−base)/base.mode:"magnet", magnetThreshold:16px; weakMagnet притягивает к OHLC только в пределах порога; percent/indexed-скейл отключают snap.e[r+n++]=i.time,e[r+n++]=-i.open,e[r+n++]=-i.high,e[r+n++]=-i.low,e[r+n++]=-i.close,e[r+n++]=i.color.r/255,...setBarData: OHLC пишутся отрицательными (экран Y вниз), цвет /255.
#define SCALE_NORMAL 0 #define SCALE_LOG 1 #define SCALE_PERCENT 2 #define SCALE_INDEXED 3\nfloat toLog(float p){...return 0.4342944819*log(m+coordOffset)+logicalOffset;}4 режима прайс-скейла в вершинном шейдере.
outColor=isRenderInteractive?vec4(uidColor,1.0)*step(0.0,mask):vColor*mask;GPU-picking: второй проход — уникальный id-цвет серии.
deltaY/100, учёт deltaMode (×120 page / ×32 line), zoomTime(x,r) якорится к курсору; шаг barSpacing 10%/единицу.scrollChart(-80·t). Wheel по оси цены: zoomPrice, 10%/щелчок.KineticAnimation(.2, 7, .997, 15), экспоненциальное затухание на rAF. Анимации видимого диапазона — без изинга (setVisibleRange/fitContent/autoscale мгновенны).min .5 … 0.5·width, дефолт 6.fly/fade/slide, дефолт 400мс, easing cubicInOut/cubicOut) — UI.DefaultAnimationDuration=400мс (timescale), AnimationDurationMs=1000мс (price/last-price). Spring-физики и cubic-bezier нет. Drag-n-drop — CSS transition: transform Nms ease.ease*»-семьи (easeNumber/easeCapture…) не существует — это были подстроки increase/release.ticker.autoStart=false; ticker.stop()); свой rAF зовёт ticker.update() только если scenes.some(isNeedRedraw())._onMousewheel=e=>{let t=e.deltaX/100,i=-e.deltaY/100;...this.model().zoomTime(n,r)...this.model().scrollChart(-80*t)}Wheel: вертикаль → зум времени (якорь у курсора), горизонталь → скролл (×80).
getPosition(e){...return t.position+this._speedPxPerMsec*(Math.pow(this._dumpingCoeff,i)-1)/Math.log(this._dumpingCoeff)}Инерция: экспоненциальное затухание (dumping=.997).
r.ticker.autoStart=!1,r.ticker.stop();const n=()=>{r?.scenes.some(a=>a.isNeedRedraw())&&r.ticker.update(),requestAnimationFrame(n)}On-demand: рендер только когда сцена «грязная».
Собственный Python-подобный язык скриптов. Редактор = CodeMirror 6 + грамматика Lezer-Python (язык name:"indie") + тема oneDark. Компиляция/исполнение — на сервере (gRPC-стрим), на клиенте только парсинг для автокомплита/линтинга. # indie:lang_version = 5.
place_order/amend_order/cancel_order), PlaceOrderBuilder, Order, Position, Stop, Commission; enum order_side/order_status/….cross/cross_over/cross_under, isnan, rgba…@indicator, @strategy, @algorithm, @plot/@line/@histogram/@fill/@marker, типизированные параметры @int/@float/@bool/@str.GetIndicatorStream (ServerStreaming) несёт IndicatorKey{indicator_id, time_frame, indicator_args[], indicator_source_code} + runtime — то есть исходник Indie уходит на бэк.Runtime = {R_PYTHON, R_WASM}, выбор RuntimeOrAuto = {ROA_AUTO, ROA_PYTHON, ROA_WASM} — сервер исполняет Indie как Python или скомпилированный WASM (auto). На клиенте этого нет.IndicatorSnapshot (вся история), далее инкрементальные IndicatorUpdate. Привязка к графику — SeriesDrawItem (oneof: line/fill/histogram/columns/marker/background/bar_color…) 1:1 с indie.plot.*.CodeMigratorApi/MigrateCode. Маркетплейс — репозиторий TPI («Explore Indicators», «Community», «My Scripts»).ut=Mt.define({name:"indie",parser:pn.configure({props:[...IfStatement:i=>/^\s*(else:|elif )/.test(i.textAfter)?...]})})Язык «indie» определён на парсере Lezer-Python с Python-отступами.
Es=r=>`# indie:lang_version = 5\nfrom indie import indicator\n\n@indicator('My Indie 1', overlay_main_pane=True)\ndef Main(self):\n return self.close[0]\n`Дефолтный шаблон индикатора.
makeEnum("...indicator.v2.Runtime",[{no:1,name:"R_PYTHON"},{no:2,name:"R_WASM"}])Серверный исполнитель: Python или WASM.
Проприетарный слой поверх LWC-модели, рендер геометрии — Pixi v7. Точки хранятся в логических координатах {time, indexOffset, price} (устойчивы к скроллу/зуму), не в пикселях.
normal/weakMagnet/magnet), magnetThreshold:16px, snap к OHLC бара под курсором; snap-to-angle округляет к 45°.pane/drawing/folder/mainSymbol/indicator/symbol; папки, DnD (dndDrawings), видимость, блокировка, перенос между пейнами, z-order.global / workspace / channel / local; рисунки на индикаторах форсятся в local; при смене sync рисунок мигрирует между сторами.const mm={time:NaN,indexOffset:0,price:NaN},pt={coordinate:mm,name:"Point"}Точка = логическая координата (время/индекс/цена), не пиксели.
getDrawingStoreBySyncSetting(t){return t==="global"?this.#t:t==="workspace"?this.#e:t==="channel"?this.#s:this.#r}4-уровневая персистентность рисунков (включая per-channel #s).
N={PANE:"pane",DRAWING:"drawing",FOLDER:"folder",MAIN_SYMBOL:"mainSymbol",INDICATOR:"indicator",SYMBOL:"symbol"}Типы узлов Object Tree.
Стакан и пузыри — собственный движок «TxOrderBook UI lib» на чистом Canvas 2D (НЕ Pixi): пять наложенных <canvas>-слоёв с DPR-масштабированием под Retina. Данные — gRPC-стрим OrderBookApi/GetOrderBookStream в Web Worker.
pushMany=SNAPSHOT (пересортировка bids desc/asks asc), pushOne=delta (insert-sorted, vol===0 ⇒ удалить уровень).computeGroups): округление к levelStep, кумулятив volSummed, volRatio; пресеты шага x1/x2/x5/x10/x25/x50/x100.canvas.width=w·devicePixelRatio; ctx.scale(dpr,dpr).tradeAggregation=300мс; размер пузыря — lerp(halfRow, 3·halfRow) по нормировке объёма на 95-й перцентиль.tradeView: bar|bubble (дефолт bar), mode: classic|pro, footprint-кластеры (clusterTimeframe дефолт 5 мин), окраска delta зелёный/красный.#1D9169, sell #E84144; пузырь = два ctx.arc() (outline+fill) + опц. градиентный «хвост».getOrderBookStream:{name:"GetOrderBookStream",I:sE,O:oE,kind:at.ServerStreaming}Источник DOM — серверный gRPC-стрим (Connect).
i.arc(b,d,m+u,0,2*Math.PI),i.fill(),i.arc(b,d,m,0,2*Math.PI),i.fill()Пузырь = два Canvas2D arc() (outline + fill), без Pixi.
computeRawR(t,e){const[s]=ri(t,[.95]);return{rRaw:s?De(e/s):1}}Размер импульса нормируется на 95-й перцентиль объёма.
Нативный тикет = Connect-RPC TradingApi (UI на Svelte 5). Брокеры Lime / J2T / Ilotcos — сторонние iframe (бэкенд Finam; J2T через MT5) с протоколом postMessage.
MARKET/LIMIT (+ stop как stop.triggerPrice поверх). TIF: GTC/IOC/FOK.triggerBy: LAST_PRICE|MARK_PRICE), маржа cross/isolated/portfolio, позиция ONE_WAY/HEDGE, reduceOnly, идемпотентность crypto.randomUUID().GetPlaceOrderDataStream.0.02%, taker 0.055%. Адаптеры: bybit (live), bybit_testnet (paper).requestInitData/requestSecurityChange/widgetUpdate/channelUpdate), синхрон инструмента через channel; URL вида lime.widget.takeprofit.com/order/index.html.const a={idempotencyToken:crypto.randomUUID(),order:{order:{case:"baseOrder",value:{side:Zi[i.direction],quantity:Ge(i.quantity),orderType:$i(i),stop:ea(i),takeProfit:Xt(...),stopLoss:Xt(...),reduceOnly:i.reduceOnly}}}};await Ji.placeOrder(a)Сборка PlaceOrder (oneof baseOrder) + Connect-RPC; идемпотентность UUID.
takerFeeRate:55e-5,makerFeeRate:2e-4Дефолтные комиссии (Bybit-подобные).
LimeOrder:{production:"https://lime.widget.takeprofit.com/order/index.html",nonProduction:".../takeprofit-widgets-lime.finam.dev/order/"}Брокер-iframe (non-prod на finam.dev → бэкенд Finam).
Connect-Web (@connectrpc/connect) поверх protobuf-es (@bufbuild/protobuf), два протокола: gRPC-web (application/grpc-web+proto) и Connect (application/connect+proto). Стриминг — Connect server-streaming по HTTP (не WebSocket), мультиплекс в SharedWorker + Comlink.
| Service typeName (takeprofit.*) | Методы |
|---|---|
| marketdata.external.quote.v1.QuoteApi | QuoteStream ★(SS), ListQuotes |
| marketdata.external.candle.v1.CandleOffsetApi | QueryCandleOffsets |
| marketdata.external.candle.v1.ExtrapolationApi | ListBars, GetBarTimestamp, GetBarsCount, ListOffsets |
| marketdata.order_book.v1.OrderBookApi | GetOrderBookStream ★(SS) |
| marketdata.external.fundamental.v2.FundamentalApi | GetFundamentalCatalogue, ListCategories, ListModes, ListFundamentals |
| marketdata.external.ratio.v2.CatalogueApi | GetRatioCatalogue, ListModes |
| indicator.controller.v2.ControllerApi | GetIndicatorStream ★(SS), GetIndicatorHistory, GetIndicatorsInfo, GetStrategyOrdersAndTradesHistory |
| indicator.code_migrator.v1.CodeMigratorApi | MigrateCode |
| indicator.strategy_report_stylist.v1 | GetStrategyReportLayout |
| trading.trading.v1.TradingApi | PlaceOrder, CancelOrder, UpdateOrder, GetEventsStream ★(SS), GetPlaceOrderData(+Stream ★SS), StoreApiKey/UpdateApiKey/ListAllApiKeyInfos/DeleteApiKey, ListAdaptors, GetAdaptorCapabilities, SetLeverage, SetMarginMode, SwitchSpotMarginTrade, SetPositionMode |
| alerts.tpi_alerts.v1.AlertsApi | Create/Get/Update/Delete/Start/Stop/ListAlerts, GetUserAlertLog, Mark/DeleteAlertLogs |
| alerts.tpi_notifications.v1.NotificationsApi | TestUserWebhook |
| alerts.tpi_popup_sender.v1.PopupSenderApi | SendUserNotification, GetUserNotificationStream ★(SS) |
| reference.external.exchange.v1.ExchangeApi | ListExchanges, ListCategories, ListAliases |
| reference.external.industry.v1.IndustryApi | GetIndustriesTree |
| reference.external.security.v1.SecurityInfoApi | ListSecurities, ListSecurityClasses |
| screener.external.v1.SearcherApi | Search, ListProperties, ListCategories, SearchWithGroups |
| screener.external.v2.CategoryApi | ListCategories, ListCategoryGroups |
| screener.external.v2.StockScreenerApi | Search |
| telegram_connector.v1.TelegramConnectorApi | GenerateAuthLink, CheckUserAuth, DisconnectTelegramAccount, SendMessage |
backend.takeprofit.com, путь /market-data/) и trading (trading.takeprofit.com, /market-data-trading/); env-карты local/dev/stage/prod.checkRefresh(), затем токен в заголовок — для trading authorization: Bearer <accessToken>, для marketdata сырой accessToken. 401 → авто-refresh.ControllerApi/GetIndicatorStream (основная серия = «индикатор»); лёгкий Bar{time,o,h,l,c,volume} (double). Candle-сообщение — decimal-обёртки + buying/selling split + tick_count.vol=0⇒remove). Свечи: SERIES_LAST (live-бар) vs SERIES_HISTORY (бэкфилл) по времени. Trades: ring-buffer cap 1000.stopStream. Keepalive — на уровне fetch-стрима; завершение по EndStreamResponse.typeName:"takeprofit.marketdata.external.quote.v1.QuoteApi",methods:{quoteStream:{name:"QuoteStream",I:dee,O:hee,kind:qi.ServerStreaming},listQuotes:{...,kind:qi.Unary}}Сервис котировок: стрим QuoteStream + унарный ListQuotes.
m?t=new SharedWorker(e,r):t=new Worker(e,r);...port.postMessage({type:"REGISTER",clientID:this.#s})Мультиплекс стримов между вкладками через SharedWorker.
pushOne=r=>{...e.bids=n(e.bids,r.bids,"desc"),e.asks=n(e.asks,r.asks,"asc"),e.updateType="UPDATE"...};pushMany=r=>{...e.updateType="SNAPSHOT"...}Orderbook: pushMany=SNAPSHOT, pushOne=delta UPDATE.
Stytch (self-hosted на api.stytch.takeprofit.com) для OAuth-старта + самописный бэкенд /api/auth/*. HTTP-клиент — ky.
public-token-live-7c58…e28f; dev на api.stytch.com.…/v1/public/oauth/google/start). Apple/SMS/OTP/magic-link — отсутствуют. Отдельный Bybit OAuth — для привязки биржи, не вход./api/auth/mfa/verify).token (все вызовы credentials:"include"), не localStorage. Refresh: GET /api/auth/token → короткоживущий accessToken → в gRPC/Connect-заголовки. 401 → авто-refresh+повтор.last_email/last_username/last_signin_method). Реферралка — window.Rewardful.const Tt={prod:{host:"https://api.stytch.takeprofit.com",token:"public-token-live-7c583685-…-1eec86c7e28f"}}Stytch public token, self-hosted домен.
const r=await ci.get("api/auth/token",{credentials:"include",headers:t?{cookie:`token=${t}`}:void 0});return{result:i.data.accessToken}Refresh: cookie-сессия token → accessToken.
#t=async(e,n,r)=>{if([401].includes(r.status))return await this.requestRefresh(),this.transport(e,n)}Авто-refresh и повтор при 401.
Раскладка и настройки — на сервере (REST /api/settings через ky), НЕ в браузере. localStorage/IndexedDB для layout не используются.
toJSON(); снимок несёт userID + version.beforeunload.prefixUrl:"/api/settings" (retry:3): rootSettings, workspaces, watchlist, settings, financials, search, indicator. Контракт: POST/PUT/GET/DELETE /api/settings[/{{ID}}]?settingType=…; dirty-check перед отправкой.window.injectSave() (localStorage-шим, не прод).openDB=0 для прочего).saveDataTreeThrottled=NL(this.saveDataTree,FBe) // FBe=5000Throttle сохранения дерева — раз в 5с (lodash).
t=hh.extend({prefixUrl:"/api/settings",retry:3});this.#e={rootSettings:new SettingsObjectService(...),workspaces:new SettingsArrayService(...),...}REST-бэкенд = ky на /api/settings, 7 сервисов по разделам.
Собственная токен-система поверх Tailwind CSS (префикс tw-, 711 утилит). Темы = статический CSS ([data-theme=slug]) для пресетов + рантайм-инъекция CSS-переменных для пользовательских.
--colors-X = hex и --colors-var-X = «R G B» (для alpha через rgb(var(--colors-var-X)/a)).matchMedia(prefers-color-scheme).--colors-chart-{{rise,fall,red,green,blue,aqua,lime,orange,purple,pink,fuchsia,teal,navy,olive,maroon,brown,grey,silver,black,white}} + фикс-массив 49 hex для дискретных серий. Свечи читают токены напрямую (upColor:"--colors-chart-rise").user-theme-${{uuid}}, экспорт window.exportTheme, ключ takeprofit.theme.css-variables.#d(r){...document.documentElement.style.setProperty(`--colors-${s}`,r[s]),document.documentElement.style.setProperty(`--colors-var-${s}`,`${n.red} ${n.green} ${n.blue}`)}Двухканальная запись токена: hex + «R G B» для Tailwind-alpha.
#i(r){document.documentElement.setAttribute("data-theme",r);...meta[theme-color]=…}Применение пресета = атрибут data-theme=<slug>.
.wasm). «WASM на C/C++» из маркетинга = серверный рантайм Indie (R_WASM) либо преувеличение.*.worker-*.js) на диск не скачаны — точный backoff-алгоритм reconnect и worker-side keepalive не вскрыты (клиентская часть стейт-машины — да).[data-theme=…] (CSS-файлы не качались)./api/settings подтверждён; серверная технология за ним — неопределима по бандлу. Обмен Stytch-токен ↔ cookie происходит на сервере (callback-парсер не на клиенте).