Self-owned crypto data infrastructure with raw chain ingestion, reusable primitive caches, canonical market reference prices, derived metrics, and API delivery.
MQNODE is being structured as a checkpoint-safe, event-driven data platform:
Bitcoin Core RPC
-> raw block ingest
-> btc_primitive_block
-> btc_primitive_10m
-> primitive_ready queue event
-> metric workers
-> metric tables
-> API / downstream systems
The canonical primitive base is 10m.
Higher intervals such as 1h and 24h should be derived from 10m, not built as separate ingestion bases.
mqnode/
api/
main.py
routes/
health.py
btc_metrics.py
registry.py
checkpoints.py
chains/
btc/
rpc.py
listener.py
ingest.py
primitive_builder.py
block_parser.py
checkpoints/
checkpoint_service.py
config/
settings.py
logging_config.py
core/
app_context.py
errors.py
utils.py
db/
connection.py
schema.sql
migrations.py
repositories.py
sql_versions/
market/
price/
checkpoints.py
composer.py
normalize.py
registry.py
runtime.py
sources/
bitstamp.py
bybit.py
binance.py
okx.py
metrics/
btc/
network/
nvt.py
miner/
miner_revenue.py
market/
market_cap.py
fee/
fee_metrics.py
queue/
redis_conn.py
jobs.py
producer.py
registry/
metric_registry.py
dynamic_loader.py
scripts/
compose_prices.py
ingest_price_source.py
init_db.py
reconcile_btc.py
seed_registry.py
backfill_btc.py
tests/
test_checkpoint.py
test_listener_automation.py
test_nvt.py
test_price_composer.py
test_primitive_builder_automation.py
test_registry.py
workers/
worker_base.py
btc_primitive_worker.py
btc_network_worker.py
btc_miner_worker.py
btc_market_worker.py
run_worker.py
Dockerfile
docker-compose.yml
docker-compose.dev.yml
nginx/
requirements.txt
.env.example
README.md
btc_primitive_blockstores reusable block-level facts from RPC ingest.btc_primitive_10mstores reusable 10-minute on-chain facts and latest known state.- Final advanced analytics do not belong in primitive tables.
Examples that do not belong in btc_primitive_10m:
nvt_rawmvrvsopr- z-scores
- rolling statistics
- strategy signals
MQNODE uses continuous 10-minute buckets, even when no block lands in a bucket.
Flow fields represent activity during the bucket and are zero-filled when the bucket is empty.
Examples:
block_countissued_sat_10mfees_sat_10mminer_revenue_sat_10mtotal_out_sat_10mtotal_fee_sat_10mtransferred_sat_10mtransferred_btc_10mtx_count_10minput_count_10moutput_count_10msegwit_tx_count_10mblock_size_total_bytes_10mblock_weight_total_wu_10mblock_vsize_total_vb_10m
State fields represent the latest known chain state as of bucket end and are forward-filled from the latest known block state.
Examples:
supply_total_satdifficulty_lastchainwork_lastbest_block_height_lasthalving_epoch
Undefined fields stay NULL, never fake 0.
Examples:
block_interval_mean_sec_10mwhen fewer than 2 blocks existblock_interval_median_sec_10mwhen fewer than 2 blocks existavg_fee_sat_10mwhen no denominator existsmedian_fee_sat_10mwhen not enough observations existavg_feerate_sat_vb_10mwhen not enough observations existsegwit_share_10mwhentx_count_10m = 0tx_size_mean_bytes_10mwhentx_count_10m = 0block_size_mean_bytes_10mwhenblock_count = 0
Price data is separated into raw venue tables plus a canonical composed table.
bitstamp_price_10mcoinbase_price_10mbybit_price_10mbinance_price_10mokx_price_10mbitfinex_price_10m
mq_btc_price_10m
mq_price_source_registry
mqnode_cloud posts Binance Spot 10-minute rows into local MQNODE through the
internal authenticated ingest API:
Production: https://api.mamakquant.com/api/v1/internal/price/source/binance/10m
Local dev: http://localhost:8000/api/v1/internal/price/source/binance/10m
The POST request must include Authorization: Bearer <MQNODE_INTERNAL_INGEST_TOKEN>.
Local MQNODE validates that token with MQNODE_INTERNAL_INGEST_TOKEN.
The default seeded Binance registry entry is remote/cloud-fed Spot mode:
{
"mode": "remote",
"api_base": "https://api.binance.com",
"endpoint": "/api/v3/klines",
"market_type": "spot",
"historical_start_utc": "2017-08-17T00:00:00Z",
"auto_historical_backfill": true
}When Binance is in mode=remote, the local price-source runtime skips the local
binance.py exchange fetch. The cloud feeder owns its own checkpoint, so remote
POST rows upsert into binance_price_10m without advancing the local Binance
source checkpoint.
For the Binance WebSocket speed lane, run the collector on a cloud/VPS host with Binance connectivity and push finalized 10m rows back to the same internal route:
PRICE_WS_SOURCE=binance
PRICE_WS_SYMBOL=BTCUSDT
PRICE_WS_CHILD_INTERVAL=1m
PRICE_WS_OUTPUT_MODE=remote_push
PRICE_WS_REMOTE_INGEST_URL=https://api.mamakquant.com/api/v1/internal/price/source/binance/10m
PRICE_WS_REMOTE_INGEST_TOKEN=<same secret as MQNODE_INTERNAL_INGEST_TOKEN>
The Binance collector uses confirmed Spot kline events only (k.x == true),
aggregates 10 confirmed 1m candles into a 10m row, and posts quality metadata
such as quality_status, data_source_mode, child candle counts, and receive
timing with the OHLCV payload. Local Malaysia MQNODE does not need direct
Binance WS connectivity.
For an existing database that was seeded before this default changed, rerun
python -m mqnode.scripts.seed_registry or update only the Binance registry row:
UPDATE mq_price_source_registry
SET
market_type = 'spot',
config_json = '{
"mode": "remote",
"api_base": "https://api.binance.com",
"endpoint": "/api/v3/klines",
"market_type": "spot",
"historical_start_utc": "2017-08-17T00:00:00Z",
"auto_historical_backfill": true
}'::jsonb,
updated_at = now()
WHERE source_name = 'binance';- Metrics do not call exchange APIs directly.
- Raw venue fetchers must write normalized OHLCV to venue tables first.
- Raw venue tables are sparse and source-factual; they do not receive fake
NULLrows. - The canonical composer builds
mq_btc_price_10mfrom available venue buckets. - When
btc_primitive_10mexists,mq_btc_price_10mis timeline-complete against that backbone. - Primitive tables join only the canonical price table.
- Missing price never blocks primitive writes.
- Missing canonical price leaves price fields
NULLwithsource_count = 0; it never writes fake0.
The phase-0 canonical composer uses volume-weighted OHLC across valid venue rows and sums reported volumes. This is intentionally simple and replaceable later.
mqnode.chains.btc.listenerreadsbtc_raw_block_ingestion:block.- It compares checkpoint height with the current Bitcoin Core node tip.
- If the node tip is ahead, it ingests blocks from
checkpoint + 1. - Each block transaction writes
btc_blocks_rawandbtc_primitive_block. - The raw checkpoint advances only after the DB write succeeds.
raw_block_readyis enqueued only after the ingest transaction succeeds.- If one block fails, the listener marks the checkpoint
errorat the last safe height and retries cleanly on the next loop.
- The primitive worker receives
raw_block_ready. - It resolves the target 10-minute bucket from
btc_primitive_block. - It rebuilds continuously from the last primitive checkpoint through the target bucket.
- Missing intermediate buckets are still written as empty 10-minute buckets.
- Flow fields are zero-filled, state fields are carried forward, and undefined derived fields stay
NULL. btc_primitive_10mis written with UPSERT, so replaying the latest bucket is safe.- The primitive checkpoint updates only after the bucket write succeeds.
primitive_readyis enqueued after commit.
When the listener is idle, it also schedules a primitive tick so the current 10-minute timeline can continue materializing even if no new block arrives.
btc-price-source-ingestorruns enabled exchange source fetchers continuously.- On first run for a source with no checkpoint, source ingestion starts from
mq_price_source_registry.config_json.historical_start_utc. - After a source checkpoint exists, source ingestion resumes from
checkpoint - 10m. - Raw venue fetchers normalize exchange OHLCV into sparse raw
*_price_10mtables. btc-price-composerusesbtc_primitive_10mas the timeline backbone when available.- It combines each timeline bucket with available raw source rows and writes
mq_btc_price_10mwith UPSERT. - If no valid venue rows exist for a bucket, it writes a canonical
NULLprice row withsource_count = 0. - The price composer checkpoint advances only after the write succeeds.
Flow:
First run: source config historical_start_utc -> raw source table -> source checkpoint
Live: checkpoint - 10m -> raw source table -> source checkpoint
Canonical: btc_primitive_10m timeline + raw source rows -> mq_btc_price_10m
- Factor workers consume
primitive_ready. - They load enabled metric rows from
metric_registry. - Each metric runs in isolation and writes its own output table.
- Worker startup includes replay from the latest primitive checkpoint so crash recovery does not depend only on queued jobs.
- Metric dependencies declared in
metric_registry.dependenciesare checked before execution.
Checkpoints are keyed by (chain, component, interval).
Important components:
btc_raw_block_ingestion:blockbtc_primitive_10m_builder:10mbtc_primitive_10m_scheduler:10mbtc_price_canonical_composer:10mbtc_metric_<metric_name>_<interval>:<interval>worker_<queue_name>:heartbeat
On failure, checkpoint status becomes error with error_message.
mqnode/db/migrations.pynow applies a tracked schema snapshot plus numbered SQL files frommqnode/db/sql_versions/.scripts/init_db.pyapplies migrations instead of relying on an untracked bootstrap only.- New additive schema changes should be introduced through a new numbered SQL file.
/health and /api/v1/btc/health surface:
- current node tip
- last raw ingested height
- last primitive built height
- last primitive bucket time
- last canonical price bucket time
- lag vs node tip
- checkpoint error visibility
- worker stale visibility through heartbeat checkpoints
- queue depth visibility for the active RQ lanes
Start production-style stack with nginx public on port 80 and internal service ports:
docker compose up -d --buildStart local dev stack with the API also published directly on port 8000:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --buildView logs:
docker compose logs -f btc-listener
docker compose logs -f btc-primitive-worker
docker compose logs -f btc-network-worker
docker compose logs -f btc-price-source-ingestor
docker compose logs -f btc-price-composer
docker compose logs -f mqnode-apiInitialize DB:
python scripts/init_db.pySeed metric registry and price source registry:
python scripts/seed_registry.pyRun continuous price source ingestion:
python -m mqnode.market.price.runtime ingest-source --all-enabledRun one-time source ingestion:
python -m mqnode.market.price.runtime ingest-source --all-enabled --once
python -m mqnode.market.price.runtime ingest-source --source binance --onceCompose canonical prices from raw venue tables:
python scripts/compose_prices.pyDocker service:
btc-price-source-ingestorCheck and reconcile BTC reorg drift:
python scripts/reconcile_btc.pyRun listener locally:
python -m mqnode.chains.btc.listenerRun a worker locally:
python -m mqnode.workers.run_worker --queue btc_primitive
python -m mqnode.workers.run_worker --queue btc_networkBackfill raw BTC blocks:
python scripts/backfill_btc.py --mode resume
python scripts/backfill_btc.py --mode full
python scripts/backfill_btc.py --mode range --start-height 0 --end-height 1000Check API through nginx:
curl http://localhost/health
curl "http://localhost/api/v1/btc/primitive?interval=10m"
curl "http://localhost/api/v1/btc/metrics/nvt?interval=10m"
curl "http://localhost/api/v1/btc/price/canonical?interval=10m"When using docker-compose.dev.yml, the API is also reachable directly:
curl http://localhost:8000/health
curl "http://localhost:8000/api/v1/btc/primitive?interval=10m"
curl "http://localhost:8000/api/v1/btc/metrics/nvt?interval=10m"
curl "http://localhost:8000/api/v1/btc/price/canonical?interval=10m"mqnode/scripts/seed_registry.py currently seeds:
nvt_rawfor10m->btc_nvt_10mnvt_rawfor1h->btc_nvt_1h
This is only an example derived metric path. The current focus remains the correctness of raw ingest, primitive build, price composition, checkpoints, and worker automation.