# How Data Moves: DataHub, Adapters, and the Python Bridge

> The runtime flow that explains every screen update: DataHub is a one-fetch/many-subscribers pub/sub keyed by topic (`market:*`, `news:*`, `broker:<id>:*`…); CacheManager backs it with a SQLite TTL store on a separate physical DB; BrokerInterface and HTTP/WebSocket clients are leaf adapters that may not call services back; PythonRunner is the subprocess bridge that crosses the C++/Python process boundary for the ~1,423 analytics and data scripts under `fincept-qt/scripts/`. Trace a quote from broker WebSocket → adapter → DataHub topic → subscribed screen and you have the system.

- Repository: Fincept-Corporation/FinceptTerminal
- GitHub: https://github.com/Fincept-Corporation/FinceptTerminal
- Human wiki: https://grok-wiki.com/public/wiki/fincept-corporation-finceptterminal-b8b64fbc871f
- Complete Markdown: https://grok-wiki.com/public/wiki/fincept-corporation-finceptterminal-b8b64fbc871f/llms-full.txt

## Source Files

- `docs/ARCHITECTURE.md`
- `fincept-qt/src/datahub`
- `fincept-qt/src/storage`
- `fincept-qt/src/network`
- `fincept-qt/src/python`
- `fincept-qt/src/trading`
- `fincept-qt/src/mcp`
- `fincept-qt/scripts`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
- [fincept-qt/src/datahub/DataHub.h](fincept-qt/src/datahub/DataHub.h)
- [fincept-qt/src/datahub/DataHub.cpp](fincept-qt/src/datahub/DataHub.cpp)
- [fincept-qt/src/datahub/Producer.h](fincept-qt/src/datahub/Producer.h)
- [fincept-qt/src/datahub/TopicPolicy.h](fincept-qt/src/datahub/TopicPolicy.h)
- [fincept-qt/src/storage/cache/CacheManager.h](fincept-qt/src/storage/cache/CacheManager.h)
- [fincept-qt/src/storage/cache/CacheManager.cpp](fincept-qt/src/storage/cache/CacheManager.cpp)
- [fincept-qt/src/storage/sqlite/CacheDatabase.h](fincept-qt/src/storage/sqlite/CacheDatabase.h)
- [fincept-qt/src/storage/sqlite/CacheDatabase.cpp](fincept-qt/src/storage/sqlite/CacheDatabase.cpp)
- [fincept-qt/src/storage/sqlite/Database.cpp](fincept-qt/src/storage/sqlite/Database.cpp)
- [fincept-qt/src/network/http/HttpClient.h](fincept-qt/src/network/http/HttpClient.h)
- [fincept-qt/src/network/websocket/WebSocketClient.h](fincept-qt/src/network/websocket/WebSocketClient.h)
- [fincept-qt/src/python/PythonRunner.h](fincept-qt/src/python/PythonRunner.h)
- [fincept-qt/src/python/PythonRunner.cpp](fincept-qt/src/python/PythonRunner.cpp)
- [fincept-qt/src/trading/BrokerInterface.h](fincept-qt/src/trading/BrokerInterface.h)
- [fincept-qt/src/trading/BrokerTopic.h](fincept-qt/src/trading/BrokerTopic.h)
- [fincept-qt/src/trading/DataStreamManager.h](fincept-qt/src/trading/DataStreamManager.h)
- [fincept-qt/src/trading/DataStreamManager.cpp](fincept-qt/src/trading/DataStreamManager.cpp)
- [fincept-qt/src/trading/websocket/AngelOneWebSocket.h](fincept-qt/src/trading/websocket/AngelOneWebSocket.h)
- [fincept-qt/docs/DATAHUB_TOPICS.md](fincept-qt/docs/DATAHUB_TOPICS.md)
</details>

# How Data Moves: DataHub, Adapters, and the Python Bridge

Every visible value on a Fincept Terminal screen — a quote in a watchlist, a news headline, a broker order line, an economic indicator — arrives through the same in-process data plane. Producers fetch once, the `DataHub` singleton fans the result out to every interested screen by topic name, and adapters at the edges (HTTP, WebSocket, broker REST, Python subprocess) never call back into business code. This page traces that path end-to-end so you can predict, given a new feature, where the wiring goes and which invariants you must not break.

The four landmarks are: `DataHub` (pub/sub layer, in-memory state with TTL), `CacheManager` over a separate physical SQLite (`cache.db`) for persistent TTL caching, the **adapter leaves** (`HttpClient`, `WebSocketClient`, `IBroker`) that may not call services back, and `PythonRunner` — the subprocess bridge that crosses the C++/Python process boundary for the ~1,423 analytics scripts under `fincept-qt/scripts/`.

## The data plane in one picture

```mermaid
flowchart TB
    subgraph Presentation
        SC[Screens / Dashboard widgets]
    end
    subgraph DataPlane["Data plane (in-process)"]
        HUB["DataHub singleton<br/>topics_, subscriptions_, scheduler_"]
        CM["CacheManager → cache.db<br/>(separate physical DB)"]
    end
    subgraph Services["Services / Producers"]
        MDS[MarketDataService]
        NWS[NewsService]
        DSM[DataStreamManager<br/>broker:* producer]
    end
    subgraph Adapters["Adapters (leaves)"]
        HC[HttpClient]
        WS[WebSocketClient / broker WS]
        PR[PythonRunner → subprocess]
    end
    subgraph Python["Python (separate process)"]
        SCR["scripts/*.py (~1,423)<br/>venv-numpy1 / venv-numpy2"]
    end
    SC -->|subscribe topic| HUB
    HUB -->|refresh topics| MDS
    HUB -->|refresh topics| NWS
    HUB -->|refresh topics| DSM
    MDS --> HC
    MDS --> PR
    NWS --> HC
    NWS --> CM
    DSM --> WS
    DSM --> HC
    PR --> SCR
    MDS -->|publish topic, value| HUB
    NWS -->|publish topic, value| HUB
    DSM -->|publish topic, value| HUB
    HUB -->|topic_updated| SC
```

The arrows go one way past the data plane: presentation → application → adapters. Adapters are leaves — `HttpClient` does not import a service; `IBroker` does not call `DataHub`. The brokerage wiring stays this way because `DataStreamManager` is the single registered hub `Producer` for the `broker:*` family, owning translation from broker callbacks into hub publishes.

Sources: [docs/ARCHITECTURE.md:48-77](docs/ARCHITECTURE.md), [fincept-qt/src/trading/DataStreamManager.cpp:156-247](fincept-qt/src/trading/DataStreamManager.cpp), [fincept-qt/src/datahub/DataHub.h:43-82](fincept-qt/src/datahub/DataHub.h)

## DataHub: one fetch, many subscribers, keyed by topic

`DataHub` is a process-wide singleton living on whichever thread first called `instance()` — in practice the main thread, since `main.cpp` warms it at startup. Subscribers attach a callback to a topic string; producers later call `publish(topic, QVariant)`. The hub keeps the latest value in memory (`TopicState::value`), so a fresh subscriber receives an immediate replay of the cached value (see `deliver_initial_value`), and the next fetch is shared by everyone — that is the "one-fetch, many-subscribers" invariant.

Topic strings are colon-separated identifiers; pattern subscriptions support a single trailing `*`. Canonical families include `market:quote:<sym>`, `news:general`, `econ:<source>:<request_id>`, and the Phase 7 broker family `broker:<broker_id>:<account_id>:<channel>[:<sym>]`. The full registry is maintained in `DATAHUB_TOPICS.md`.

```cpp
// fincept-qt/src/datahub/DataHub.h — the public API
QMetaObject::Connection subscribe(QObject* owner, const QString& topic,
                                  std::function<void(const QVariant&)> slot);
QMetaObject::Connection subscribe_pattern(QObject* owner, const QString& pattern,
    std::function<void(const QString&, const QVariant&)> slot);
void publish(const QString& topic, const QVariant& value);
void publish_error(const QString& topic, const QString& error);
QVariant peek(const QString& topic) const;       // TTL-checked, no fetch
void request(const QString& topic, bool force = false);
```

Three behavioural invariants matter when reading new screen code:

- **Owner-scoped lifetime.** Every `subscribe()` takes a `QObject* owner`; when the owner is destroyed, subscriptions auto-cancel. There is no manual `unsubscribe()` in normal use. The hub tracks owners in `owner_topics_` / `owner_patterns_` for fast cleanup.
- **Thread-safe publish.** `publish()` can be invoked from any thread — `QNetworkAccessManager` callbacks, broker WS parser threads, Python finished slots — and the hub marshals to its own thread via queued invocation. Slot dispatch then lands on the subscriber's thread via Qt's `AutoConnection`.
- **Last-known-good on failure.** `publish_error()` does *not* clear the cached value. Subscribers keep seeing the previous good payload while `topic_error(topic, error)` fires for UI surfacing. The cached value only changes on a successful `publish()`.

Sources: [fincept-qt/src/datahub/DataHub.h:43-203](fincept-qt/src/datahub/DataHub.h), [fincept-qt/src/datahub/DataHub.cpp:56-102](fincept-qt/src/datahub/DataHub.cpp), [fincept-qt/docs/DATAHUB_TOPICS.md:1-66](fincept-qt/docs/DATAHUB_TOPICS.md)

### TopicPolicy: TTL, min interval, push-only, coalesce

Per-topic refresh is controlled by `TopicPolicy`. A 1-second `QTimer` (`scheduler_`) walks the topic state every tick and asks a registered `Producer` to refresh anything stale — but only if there are live subscribers, no in-flight request, and the per-producer rate cap allows it.

| Field | Default | Effect |
|---|---|---|
| `ttl_ms` | 30 000 | Cached value is considered fresh for this many ms; scheduler waits until expiry to refetch. |
| `min_interval_ms` | 5 000 | Hard floor between two `refresh()` calls on the same topic, regardless of subscriber count. |
| `refresh_timeout_ms` | 30 000 | If neither `publish()` nor `publish_error()` arrives in this window, the hub clears `in_flight` and logs a warning. |
| `push_only` | false | Scheduler ignores the topic — producer pushes when feed messages arrive (e.g. WebSocket ticks). |
| `coalesce_within_ms` | 0 | Backpressure window for high-rate streams; only the latest payload within the window is fanned out. |
| `pause_when_inactive` | false | Suppresses fan-out (not cache update) when the subscriber's `WindowFrame` is hidden/minimised. Used sparingly for high-frequency streams. |
| `drop_on_idle` | false | Drops the entire `TopicState` when the last subscriber leaves. For unbounded topic keys (per-run agent outputs). |

Sources: [fincept-qt/src/datahub/TopicPolicy.h:1-60](fincept-qt/src/datahub/TopicPolicy.h), [fincept-qt/src/datahub/DataHub.cpp:61-92](fincept-qt/src/datahub/DataHub.cpp)

### Request coalescing

Producers don't get hammered when multiple panels light up at once. `DataHub::request()` queues topics per producer into `coalesce_pending_` and arms a single-shot `coalesce_timer_` (default 100 ms). When it fires, `flush_coalesced_requests()` collapses the burst into one `Producer::refresh(topics)` per producer. Tests commonly set the window to 0 for synchronous dispatch via `set_coalesce_window_ms()`.

Sources: [fincept-qt/src/datahub/DataHub.h:316-326](fincept-qt/src/datahub/DataHub.h), [fincept-qt/src/datahub/DataHub.cpp:71-92](fincept-qt/src/datahub/DataHub.cpp)

## CacheManager: separate physical DB, sibling not backing

`CacheManager` is *not* the in-memory store behind `DataHub` — `DataHub::TopicState::value` is a plain `QVariant` field, in RAM. `CacheManager` is a parallel data-plane component: a SQLite TTL key/value store on a **separate physical DB** (`fincept_cache` connection, `cache.db` file), used by services to avoid re-hitting upstream HTTP/Python sources across app restarts. The main DB (`fincept_main`, `fincept.db`) holds authoritative state (auth, watchlists, portfolios, repositories); the cache DB holds disposable rows.

```cpp
// fincept-qt/src/storage/sqlite/CacheDatabase.cpp
db_ = QSqlDatabase::addDatabase("QSQLITE", "fincept_cache");
db_.setDatabaseName(path);
// PRAGMA journal_mode = WAL, synchronous = NORMAL, busy_timeout = 3000…
// CREATE TABLE unified_cache (key, value, category, ttl_seconds, expires_at, …)
```

`CacheManager` operations are mutex-serialised at the `CacheDatabase` layer because callbacks may run on worker threads (`HttpClient`, `PythonRunner`). A startup sweep deletes expired rows so the table doesn't accumulate across sessions, and `expires_at > datetime('now')` is checked on every read.

Two notes that matter for predicting behaviour:

- The cache DB also stores `tab_sessions` and `screen_state` (per-tab UI state, screen restore data). The `synchronous=NORMAL` choice — not `OFF` — exists because losing those tables on power-loss would visibly break the next session.
- There is no in-memory layer in front of `CacheManager`. The single SQLite path is the source of truth: avoids double-storage and "stale-in-memory while fresh-on-disk" confusion.

Sources: [fincept-qt/src/storage/cache/CacheManager.h:9-33](fincept-qt/src/storage/cache/CacheManager.h), [fincept-qt/src/storage/cache/CacheManager.cpp:31-70](fincept-qt/src/storage/cache/CacheManager.cpp), [fincept-qt/src/storage/sqlite/CacheDatabase.cpp:14-187](fincept-qt/src/storage/sqlite/CacheDatabase.cpp), [docs/ARCHITECTURE.md:90-91](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:187-192](docs/ARCHITECTURE.md)

## Adapters are leaves: HTTP, WebSocket, and brokers

The dependency rule in `docs/ARCHITECTURE.md` is explicit: *adapters are leaves; two services may share an adapter; an adapter may not call a service.* Three adapter surfaces follow this:

### HttpClient

A `QNetworkAccessManager` wrapper with context-scoped callbacks — callers pass `this` so the callback is dropped if the requester dies before the reply arrives. Singleton, GUI-thread safe, returns `Result<QJsonDocument>`. It does not import any service header.

```cpp
// fincept-qt/src/network/http/HttpClient.h
using JsonCallback = std::function<void(Result<QJsonDocument>)>;
void get(const QString& url, JsonCallback callback, const QObject* context = nullptr);
```

### WebSocketClient

Thin Qt6 WebSockets wrapper with auto-reconnect (`MAX_RECONNECT_ATTEMPTS = 10`). The socket is heap-allocated and parented to `this` so `moveToThread` on the wrapper relocates the socket plus its internal `QSocketNotifier`/`QTimer` children — a subtle invariant the comment in the header preserves.

### IBroker

The trading abstraction is wider than the network adapters but obeys the same rule. `IBroker` (32 virtual methods covering auth, orders, portfolio, market data, GTT, margin, WebSocket adapter name) is implemented by each of 16 brokers under `fincept-qt/src/trading/brokers/<name>/`. Brokers do not know `DataHub` exists. `DataStreamManager` is the single registered `Producer` for `broker:*` and the only place where broker callbacks are translated into hub publishes — see the trace below.

Sources: [docs/ARCHITECTURE.md:54-77](docs/ARCHITECTURE.md), [fincept-qt/src/network/http/HttpClient.h:14-45](fincept-qt/src/network/http/HttpClient.h), [fincept-qt/src/network/websocket/WebSocketClient.h:13-55](fincept-qt/src/network/websocket/WebSocketClient.h), [fincept-qt/src/trading/BrokerInterface.h:74-294](fincept-qt/src/trading/BrokerInterface.h)

## PythonRunner: the C++/Python process boundary

`PythonRunner` is the subprocess bridge for every Python entry point in `fincept-qt/scripts/` — analytics, agents, broker connectors, data fetchers. The repo carries ~1,423 such scripts (the architecture doc's figure; a current `find` in this checkout reports 1,425 `.py` files), organised under twelve top-level subpackages plus a flat top-level group:

```
fincept-qt/scripts/
├── agents/ algo_trading/ alpha_arena/ agno_trading/ ai_quant_lab/
├── Analytics/ exchange/ mcp/ strategies/ technicals/
├── vision_quant/ voice/
└── *.py   (≈320 flat data fetchers — yfinance_data.py, fred_data.py, …)
```

The runner is the only place that crosses the C++/Python process boundary, and it makes a small number of opinionated choices that callers should know about.

### Venv routing and process pacing

`PythonRunner::run()` resolves Python at startup, preferring UV-managed venvs from `PythonSetupManager` (`venv-numpy2` default, `venv-numpy1` for legacy NumPy 1.x libs like vectorbt/gluonts), falling back to a venv next to the executable and finally to system `python3`. Concurrency is capped at three processes (`DEFAULT_MAX_CONCURRENT`) and surplus requests queue in `queue_`. Cold start is 0.5–1.5 s per call; results are returned as a `PythonResult { success, output, error, exit_code }` on the Qt event loop.

```cpp
// fincept-qt/src/python/PythonRunner.h
void run(const QString& script, const QStringList& args, Callback cb,
         StreamCallback on_line = {});
void run_code(const QString& code, Callback cb);   // inline cell code via temp file
static constexpr int DEFAULT_MAX_CONCURRENT = 3;
```

### Thread-affinity guard

Worker threads (notably MCP tool handlers) are allowed to call `run()`. The runner detects `QThread::currentThread() != this->thread()` and re-invokes itself via `QMetaObject::invokeMethod(... Qt::QueuedConnection)`. The hidden hazard this prevents is `new QProcess(this)` on a foreign thread — leaving the process with wrong thread affinity and corrupting the heap silently during destruction.

### Standard environment and credential discipline

Every spawn goes through `build_python_env()`, which pins encoding (`PYTHONIOENCODING=utf-8`, `PYTHONUNBUFFERED=1`), sets `FINCEPT_DATA_DIR` / `FINAGENT_DATA_DIR`, prepends `scripts_dir` to `PYTHONPATH`, and injects API keys from `SecureStorage`. Critically, any *other* credential-shaped env var the user inherited from their shell is stripped before the subprocess sees it — preventing leakage via `/proc/<pid>/environ`. That stripping is the inverse of the allow-list and is the reason `os.environ.get("FRED_API_KEY")` in a script reliably returns the value stored in SecureStorage rather than whatever shell quirk the user has.

### Daemon fast-path

For `yfinance_data.py` (called from many screens for quotes, news, financials, history), single-shot calls bypass the subprocess + import cost by routing to `PythonWorker` — a long-lived daemon. The runner translates CLI args into a JSON action+payload and submits it. If the action is unknown, the subprocess path runs as a fallback. This is why the same script call from Python may cost milliseconds on hot paths and ~3 s on cold paths.

### Sub-package vs flat scripts

If the script path contains `/` and every ancestor has an `__init__.py`, `PythonRunner` invokes it as `python -m pkg.sub.module` so relative imports resolve. Otherwise it falls back to direct script invocation so the script's own `sys.path.insert(...)` bootstrap can run.

### Argument spilling

Args larger than 8 KB are written to a temp file and passed as `@/path/to/file`; the Python script reads and deletes it. This sidesteps the Windows 32 KB command-line limit and is the contract behind the `resolve_arg` helper used by data scripts.

Sources: [fincept-qt/src/python/PythonRunner.h:14-103](fincept-qt/src/python/PythonRunner.h), [fincept-qt/src/python/PythonRunner.cpp:35-117](fincept-qt/src/python/PythonRunner.cpp), [fincept-qt/src/python/PythonRunner.cpp:200-265](fincept-qt/src/python/PythonRunner.cpp), [fincept-qt/src/python/PythonRunner.cpp:398-591](fincept-qt/src/python/PythonRunner.cpp), [docs/ARCHITECTURE.md:202-209](docs/ARCHITECTURE.md)

## End-to-end: a quote from broker WebSocket to a subscribed screen

The single most useful trace for understanding the runtime is a tick flowing from a broker WebSocket all the way to a panel repaint. Every claim below is a real link in the code, not an analogy.

```mermaid
sequenceDiagram
    autonumber
    participant WS as AngelOneWebSocket / ZerodhaWebSocket
    participant ADS as AccountDataStream
    participant DSM as DataStreamManager<br/>(Producer for broker:*)
    participant HUB as DataHub
    participant SCR as Screen / Widget<br/>(subscriber)

    WS->>WS: on_binary_message: parse_tick() (binary frame)
    WS->>ADS: emit tick_received(AoTick)
    ADS->>DSM: emit quote_updated(account, symbol, BrokerQuote)
    DSM->>HUB: publish("broker:angelone:default:quote:<sym>", QVariant::fromValue(quote))
    Note over HUB: TopicPolicy: ttl 5s / min_interval 1s<br/>(ticks family: push_only + coalesce 100ms)
    HUB->>SCR: topic_updated(topic, value) → slot on subscriber thread
```

1. The broker WS adapter (`AngelOneWebSocket`, `ZerodhaWebSocket`) is a pure leaf — it parses binary tick frames (`parse_tick`) and emits a Qt signal `tick_received(AoTick)`. It does not import `DataHub`.
2. `AccountDataStream` owns the per-account socket lifecycle and translates broker-specific ticks into a unified `BrokerQuote`, emitting `quote_updated(account_id, symbol, quote)`.
3. `DataStreamManager`, which implements `fincept::datahub::Producer` with `topic_patterns() == {"broker:*"}`, has wired every stream's `quote_updated` to its own `on_quote_for_hub` slot.
4. `on_quote_for_hub` builds the canonical topic string via `broker_topic(broker_id, account_id, "quote", symbol)` and calls `DataHub::publish(topic, QVariant::fromValue(quote))`.
5. The hub stores the value, looks up the topic's policy, and fans out via `emit_to_subscribers` — broker `ticks:<sym>` topics carry `push_only=true` and `coalesce_within_ms=100`, so a burst of 30 ticks within 100 ms delivers exactly one payload (the latest) to each subscriber.
6. Screens subscribed via `subscribe_pattern("broker:angelone:default:quote:*", …)` receive `(topic, value)` on their own thread and repaint.

The policies that govern this fan-out are not buried in random services — they live in one place, `DataStreamManager::ensure_registered_with_hub()`:

```cpp
// fincept-qt/src/trading/DataStreamManager.cpp:195-228
hub.set_policy_pattern("broker:*:*:positions",  /* ttl 5s,  min 3s   */);
hub.set_policy_pattern("broker:*:*:orders",     /* ttl 5s,  min 3s   */);
hub.set_policy_pattern("broker:*:*:balance",    /* ttl 30s, min 10s  */);
hub.set_policy_pattern("broker:*:*:holdings",   /* ttl 30s, min 10s  */);
hub.set_policy_pattern("broker:*:*:quote:*",    /* ttl 5s,  min 1s   */);
hub.set_policy_pattern("broker:*:*:ticks:*",    /* push_only, coalesce 100ms */);
```

Sources: [fincept-qt/src/trading/websocket/AngelOneWebSocket.h:63-91](fincept-qt/src/trading/websocket/AngelOneWebSocket.h), [fincept-qt/src/trading/DataStreamManager.h:39-78](fincept-qt/src/trading/DataStreamManager.h), [fincept-qt/src/trading/DataStreamManager.cpp:156-294](fincept-qt/src/trading/DataStreamManager.cpp), [fincept-qt/src/trading/BrokerTopic.h:17-30](fincept-qt/src/trading/BrokerTopic.h), [fincept-qt/docs/DATAHUB_TOPICS.md:52-71](fincept-qt/docs/DATAHUB_TOPICS.md)

## Invariants and failure modes worth memorising

| Invariant | Where it lives | What breaks if you violate it |
|---|---|---|
| Adapters are leaves | `docs/ARCHITECTURE.md` rules 2–3 | Cycle between trading and a service via a broker include → unbuildable target or runtime singleton race. |
| `publish()` is thread-safe; subscribe slot runs on subscriber thread | `DataHub::publish` queued invocation | Calling Qt UI APIs from inside a worker-thread publish path → Qt fatal. |
| Last-known-good on failure | `publish_error` does not clear value | UI flashing blank on transient HTTP errors. |
| Owner-scoped subscriptions | `subscribe(QObject* owner, …)` + `on_owner_destroyed` | Use-after-free if owner-less raw pointers subscribe. |
| Cache DB is separate, durable, mutex-serialised | `CacheDatabase` connection `fincept_cache` | Mixing cache rows into the main DB makes "clear cache" risky. |
| PythonRunner is thread-affinity guarded | `run()` re-marshals via `QueuedConnection` | Heap corruption from foreign-thread `QProcess` destruction. |
| Credential allow-list, strip everything else | `build_python_env()` strips unmanaged credential vars | API keys leak into Python subprocesses via inherited shell env. |
| Only one producer per topic family | `DataStreamManager` for `broker:*` | Duplicate publishes / racy refresh scheduling. |

Sources: [docs/ARCHITECTURE.md:72-77](docs/ARCHITECTURE.md), [fincept-qt/src/datahub/DataHub.h:115-132](fincept-qt/src/datahub/DataHub.h), [fincept-qt/src/python/PythonRunner.cpp:401-421](fincept-qt/src/python/PythonRunner.cpp), [fincept-qt/src/python/PythonRunner.cpp:241-265](fincept-qt/src/python/PythonRunner.cpp)

## Summary

The Fincept Terminal data plane works because every screen, every service, and every external integration agrees on the same shape: producers fetch once and publish typed values to a `DataHub` topic; subscribers attach by topic and let the hub handle TTL, coalescing, pause-when-inactive, last-known-good, and lifetime. `CacheManager` over a separate `cache.db` adds cross-session persistence without contaminating the authoritative `fincept.db`. Adapters — `HttpClient`, `WebSocketClient`, `IBroker`, `PythonRunner` — sit at the bottom of the dependency graph and never look upward, which is what lets the ~1,423 Python scripts and the 16 brokers coexist without circular wiring. Trace any value backwards from a screen and you should land on exactly one `Producer::refresh()` or one push path, with the topic policy explaining the timing.

Sources: [docs/ARCHITECTURE.md:1-77](docs/ARCHITECTURE.md), [fincept-qt/src/datahub/DataHub.cpp:56-115](fincept-qt/src/datahub/DataHub.cpp)
