Skip to content
Closed

ups #6991

Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
84b868a
Add laptop quant desk: csp + gs-quant + ArcticDB EMA crossover sweep
claude Nov 8, 2026
4ba420a
Merge pull request #1 from joseedson18jc/claude/quirky-archimedes-ytfumc
joseedson18jc Nov 8, 2026
4495e5f
Add multi-year regime study: trend filters, stay-flat, mean reversion
claude Nov 8, 2026
b88fd08
Add multi-year study results: report, charts, per-sweep tables
claude Nov 8, 2026
6d32d34
Robust construction: long/flat, whole-grid ensembles, vol targeting
claude Nov 8, 2026
18d063c
Solve study caveats: next-open execution, honest selection, more coin…
claude Nov 9, 2026
ec2f487
Study outputs on 2017+ BTC history at 15 bps next-open pricing
claude Nov 9, 2026
b83057a
Deflated Sharpe: no-skill dispersion and vol-matched buy & hold compa…
claude Nov 9, 2026
0e2fbc3
Robustness, multi-coin and paper outputs (run before the DSR fix)
claude Nov 9, 2026
1d7c220
Final outputs: corrected Deflated Sharpe, robustness, 9 coins, paper …
claude Nov 9, 2026
8dbea2c
Test the open issues: long-only hold-outs, survivorship-free universe…
claude Nov 9, 2026
61c2596
Full run on fresh data: study, robustness and coin outputs (report pe…
claude Nov 9, 2026
0983482
Full run: long-only hold-outs, survivorship-free universe, Bitstamp/C…
claude Nov 9, 2026
1148d50
README: results of the full run, what is still open, current costs an…
claude Nov 9, 2026
173fe7e
quant_desk: use one import style per module (CodeQL py/import-and-imp…
claude Nov 9, 2026
991b598
Merge branch 'Tracer-Cloud:main' into main
joseedson18jc Nov 9, 2026
84bb32f
Apply suggestion from @greptile-apps[bot]
joseedson18jc Nov 9, 2026
c9402fa
Fix initial EMA entry logic and ignore generated output artifacts.
joseedson18jc Nov 9, 2026
2409e03
quant_desk: always_in opens right after warm-up instead of waiting fo…
claude Nov 9, 2026
92d2a91
quant_desk: stop committing generated output
claude Nov 9, 2026
1f59648
Merge main into claude/quirky-archimedes-ytfumc
claude Nov 9, 2026
2fba47f
Merge main into claude/quirky-archimedes-ytfumc
claude Nov 9, 2026
1aa146e
Merge pull request #2 from joseedson18jc/claude/quirky-archimedes-ytfumc
joseedson18jc Nov 9, 2026
8d77aba
quant_desk README: re-checked results after the always_in entry fix
claude Nov 9, 2026
c2a99f6
Merge pull request #3 from joseedson18jc/claude/quirky-archimedes-ytfumc
joseedson18jc Nov 9, 2026
436c565
ci: run cli-live-agent without live_llm tests outside Tracer-Cloud/op…
claude Nov 9, 2026
758f74d
Merge pull request #4 from joseedson18jc/claude/quirky-archimedes-ytfumc
joseedson18jc Nov 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions quant_desk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv/
data/
__pycache__/
.pytest_cache/
.ruff_cache/
# Reports, charts, CSVs and logs are generated by run_desk.py / run_history.py / run_paper.py.
output/
176 changes: 176 additions & 0 deletions quant_desk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Mesa quant no laptop — BTC/USDT, csp + gs-quant + ArcticDB

Pipeline completo e local (sem caixa preta: código e dados ficam com você):

1. baixa 30 dias de barras de 1 minuto de BTC/USDT da API pública da Binance (sem chave);
2. reamostra para 15m e grava as duas séries numa biblioteca local **ArcticDB** (LMDB);
3. lê de volta do ArcticDB e calcula EMA, Bandas de Bollinger e RSI com
`gs_quant.timeseries`, mais um perfil de volume (POC e value area de 70%);
4. reproduz as barras como stream no **csp** e roda um cruzamento de EMA long/short com
taxa de 2 bps;
5. varre **212 combos** (rápida 4–24 passo 2 × lenta 12–96 passo 4, com lenta ≥ 1,5 × rápida)
num único grafo csp, classifica por Sharpe, divide os 30 dias em 4 janelas e rotula cada
combo como **platô** ou **pico**;
6. desenha cada LONG e SHORT na barra exata e grava o log do agente com o mesmo timestamp;
7. roda um **walk-forward** (escolhe na janela k, opera na k+1) para comparar com a "tela".

## Rodar

Pré-requisitos: Python 3.11+ (testado em 3.13) e acesso de rede a `data-api.binance.vision`.

```bash
cd quant_desk
python -m venv .venv # ou: uv venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python run_desk.py # baixa dados novos e roda tudo (~20 s)
.venv/bin/python run_desk.py --no-fetch # reaproveita as barras já gravadas no ArcticDB
.venv/bin/python run_history.py # tudo: estudo, robustez, moedas, universo, corretoras (~30 min)
.venv/bin/python run_history.py --no-fetch
.venv/bin/python run_paper.py # paper trading: um passo (ver abaixo)
.venv/bin/python run_paper.py --loop # paper trading 24/7
.venv/bin/python -m pytest # testes (dados sintéticos, sem rede)
```

Saídas em `output/` (geradas a cada execução; não são versionadas, ficam fora do git):

| arquivo | conteúdo |
|---|---|
| `REPORT.md` | ranking, platô vs pico, walk-forward, indicadores na última barra |
| `trades.png` | preço, EMAs, Bollinger, todos os trades, RSI, equity vs buy & hold, perfil de volume |
| `trades_zoom.png` | últimos 2 dias em candles, cada trade rotulado com a hora da barra |
| `sweep_heatmap.png` | Sharpe nos 212 combos, picos e platôs marcados |
| `windows.png` | equity dos 5 melhores dentro de cada janela |
| `walk_forward.png` | walk-forward fora da amostra vs a tela e buy & hold |
| `agent.log` | uma linha por trade, com o timestamp da barra que disparou |
| `sweep.csv` | os 212 combos com todas as métricas |

O ArcticDB fica em `data/arctic` (símbolos `BTCUSDT_1m`, `BTCUSDT_15m`,
`BTCUSDT_15m_ema_sweep`; cada execução grava uma nova versão).

## Estudo de vários anos (`run_history.py`)

Barras de 15m de BTC/USDT desde ago/2017 (bear de 2018, bull de 2021, crash de 2022, bull de
2024–25 e a lateralização recente), reamostradas para 1h, 4h, 8h e 1d e gravadas no ArcticDB.
Roda em processos paralelos. Cada variante é varrida na grade inteira em cada tamanho de barra:

| variante | o que faz |
|---|---|
| `always_in` | original: cruzamento de EMA, sempre long ou short |
| `gap_cooldown` | só entra com distância entre EMAs ≥ 0,25 ATR e espera 4 barras entre trades; fica flat no resto |
| `flat_er` | segue a direção das EMAs só com eficiência (ER 20) ≥ 0,30; flat no resto |
| `flat_adx` | idem com ADX 14 ≥ 25 |
| `flat_adx_gap_cd` | ADX + distância mínima + cooldown |
| `long_flat` | comprado quando EMA rápida > lenta, flat no resto (nunca vendido) |
| `long_flat_gap_cd` | long/flat + distância mínima + cooldown |
| `long_flat_adx` | long/flat só com ADX 14 ≥ 25 |
| `mr_always` | reversão à média: compra abaixo da banda de Bollinger, vende acima, sai na média |
| `mr_chop` | igual, mas só quando o mercado está lateral (ER < 0,30) |

Os filtros usam valores de livro e **não foram otimizados**. A história é dividida em 8 janelas;
platô exige vizinhos com ≥ 50% do Sharpe e lucro em ≥ 6 de 8 janelas. Tudo é recalculado a
15 bps com execução na abertura seguinte (ver abaixo) e passa por walk-forward de 7 passos.

### Construção robusta (sem escolher parâmetro)

Em vez de escolher um combo, cada variante opera **a grade inteira de uma vez**: a exposição é a
média das posições dos 212 combos, numa única conta (taxa sobre a mudança líquida). Cada uma é
avaliada em tamanho cheio e com **alvo de volatilidade** (exposição × min(1, 40% / vol de 30
dias), ajustada 1×/dia, sem alavancagem). O buy & hold recebe o mesmo alvo de vol, então a
comparação é justa.

Regra definida **antes** de ver os resultados: em W2–W8 a 15 bps, Sharpe maior **e** drawdown
máximo menor que o buy & hold equivalente. **Robusto** = passa em pelo menos dois tamanhos de
barra vizinhos (15m → 1h → 4h → 8h → 1d).

Saídas em `output/history/`: `HISTORY_REPORT.md`, **`ROBUSTNESS_REPORT.md`**, `summary.csv`,
`sweeps/` (uma tabela por variante e barra), `candidates.csv`, `execution_test.csv`,
`meta_walk_forward.csv`, `deflated_sharpe.csv`, `sensitivity.csv`, `assets.csv`,
`portfolio.csv`, `long_short_holdout.csv`, `universe.csv`, `universe_membership.csv`,
`universe_casualties.csv`, `venues.csv` e os gráficos `wf_sharpe.png`, `heatmaps_trend.png`,
`heatmaps_mean_reversion.png`, `wf_equity.png`, `candidates.png`, `portfolio.png`, `holdouts.png`.

Rede necessária: `data-api.binance.vision`, `www.bitstamp.net`, `api.exchange.coinbase.com`.

### Custos e execução (premissas principais)

Toda decisão é tomada no fechamento da barra e **executada na abertura da barra seguinte**,
pagando **15 bps** por unidade de mudança de posição (10 de taxa taker + 5 de slippage).
2 bps (VIP/maker) aparece só como referência otimista. O BTC agora começa em ago/2017, então a
amostra inclui o bear de 2018 e o crash de 2022.

### Checagens de robustez (`ROBUSTNESS_REPORT.md`)

| ressalva | o que foi feito |
|---|---|
| escolher a melhor linha é seleção dentro da amostra | walk-forward "meta" escolhe variante, tamanho e barra só com dados passados + Deflated Sharpe Ratio |
| filtros com valores de livro | grade de sensibilidade (vol alvo, gap, cooldown, ADX) com o pior caso, sem escolher nada |
| "não operar" vencendo o walk-forward | só combos com ≥ 3 trades na janela de treino podem ser escolhidos |
| 2 bps é otimista | 15 bps + execução na abertura seguinte em todos os números principais |
| um ativo, amostra de alta | BTC desde 2017, os mesmos livros (sem reajuste) em 9 moedas e num portfólio igual-peso |
| execução idealizada | teste com fechamento, abertura seguinte e uma barra de atraso |
| long-only escolhido depois de ver os dados | cada livro long/short comparado ao seu gêmeo long/flat só em dados que a decisão nunca viu: BTC Bitstamp 2013–2017, BTC Binance 2017–2021, outras moedas e o universo sem viés |
| viés de sobrevivência | universo point-in-time: todo mês, top 10 por volume entre **todos** os pares USDT que a Binance já listou (incl. deslistados); tickers reaproveitados (LUNA antiga → nova) são separados |
| só Binance | os mesmos livros de BTC na Bitstamp (desde 2013) e na Coinbase (desde 2015) |

### Paper trading

`run_paper.py` opera no papel o livro registrado antes dos testes (`PAPER_BOOKS` em
`desk/config.py`: BTC 4h `long_flat_gap_cd` com alvo de vol). Cada decisão é gravada num
ledger que só aceita inclusões (ArcticDB, biblioteca `paper`) no momento em que é tomada e
nunca é recalculada. Se o bot ficou desligado, as decisões atrasadas são marcadas `late` e só
valem a partir de quando foram tomadas. Status em `output/paper/STATUS.md`.

```bash
nohup .venv/bin/python run_paper.py --loop >> output/paper/paper.log 2>&1 & # 24/7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Background trader never starts

The documented background command fails on a fresh checkout because output/paper/ does not exist. The shell opens the redirected log before Python starts, so run_once never gets a chance to create the directory.

Add mkdir -p output/paper before this command so a new user can start the trader.

# ou cron a cada 4h: 2 */4 * * * cd /caminho/quant_desk && .venv/bin/python run_paper.py
```

## Resultado da última execução completa

Tudo a 15 bps com execução na abertura seguinte, fora da amostra (W2–W8). Conferido de novo
depois da correção da entrada do `always_in` (agora abre logo após o aquecimento): nenhum número
abaixo mudou; só algumas linhas `always_in` das tabelas long/short do relatório (barras diárias).
Detalhes em `output/history/ROBUSTNESS_REPORT.md`, gerado por `run_history.py`.

- **Escolha sem hindsight** (meta walk-forward entre 80 livros): Sharpe 0,96 vs 0,81 do buy &
hold, drawdown −44% vs −77% — mas retorno total **menor** (+632% vs +1080%). O ganho é risco
menor, não mais dinheiro.
- **Pode ser sorte?** Deflated Sharpe: 1 de 80 livros é significativo sozinho e **nenhum** tem
Sharpe significativamente acima do buy & hold.
- **Long-only escolhido depois de ver os dados**: em dados que a decisão nunca viu, long/flat
teve Sharpe ≥ long/short em 98% de 180 comparações e o lado vendido perdeu dinheiro em 87%.
Exceção: Bitstamp 2014 (bear), onde vender rendeu mais, com Sharpe menor.
- **Viés de sobrevivência**: universo point-in-time com 199 pares deslistados de volta. Todos os
livros long/flat passam (8h com alvo de vol: Sharpe 1,10 vs 0,52, drawdown −30% vs −61%); o
buy & hold igual-peso do mesmo top 10 caiu até −95%. O viés antigo era pequeno (~0,05 de Sharpe).
- **Outras corretoras**: BTC 2013–2017 na Bitstamp, que nenhuma decisão viu: 18/18 livros passam.

Continua aberto: a escolha long-only pode ser testada mas não "desvista"; moedas que nunca
foram listadas na Binance não entram; tudo é cripto e histórico. O único teste limpo é o
paper trading rodando por semanas **na sua máquina** (`run_paper.py --loop`).

## Regras de execução da tela de 30 dias (`run_desk.py`)

- As barras são carimbadas no **fechamento**. O cruzamento na barra *t* executa no fechamento
de *t* e só ganha o movimento de *t* → *t+1*. Um teste injeta lookahead e verifica que ele é pego.
- A taxa é 2 bps por unidade de mudança de posição: entrada 2 bps, virada long↔short 4 bps.
- A estratégia abre na direção das EMAs na primeira barra após o aquecimento (sem esperar um
cruzamento) e depois vira a cada cruzamento: está sempre long ou short (stop-and-reverse).
- Sharpe anualizado com √(365 × 96) sobre retornos de 15m, já descontadas as taxas.
- **Platô**: os vizinhos na grade (±1 passo) mantêm ≥ 50% do Sharpe **e** o combo é positivo
em ≥ 3 de 4 janelas. **Pico**: qualquer outro combo com Sharpe > 0.

## Leia antes de confiar

- O melhor combo é escolhido nos mesmos dados em que é mostrado. É uma tela, não prova.
Olhe o walk-forward e o `REPORT.md`, que mostra quanto do resultado veio de um único dia.
- 30 dias geram só 3 passos de walk-forward. Rode com mais histórico (`LOOKBACK_DAYS` em
`desk/config.py`) antes de concluir qualquer coisa.
- Faça paper trade por semanas antes de pensar em operar ao vivo (`run_paper.py`).
- **Licença do ArcticDB**: Business Source License 1.1. Uso em produção é permitido, exceto
oferecer o ArcticDB como serviço de banco de dados. Cada versão vira Apache 2.0 dois anos
após o lançamento. Confira os termos da versão que você usa antes de qualquer uso comercial.
- gs-quant não tem função de perfil de volume; aquele cálculo é um histograma próprio
(`desk/indicators.py`).
- O BTC/**USDT** da Binance é o dado principal. BTC/USD da Bitstamp e da Coinbase confirmam o
resultado no BTC (correlação diária 0,997); as altcoins continuam só na Binance.
1 change: 1 addition & 0 deletions quant_desk/desk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Laptop quant desk: Binance bars -> ArcticDB -> gs-quant indicators -> csp strategy replay."""
88 changes: 88 additions & 0 deletions quant_desk/desk/books.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Shared plumbing for the hold-out studies: load stored 4h bars at any bar size, compute
whole-grid ensemble exposures in worker processes, and score a book on a span."""

from __future__ import annotations

import multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor

import pandas as pd

from desk import config, data, ensemble, filters, history, sweep, variants

ALL_BOOKS = tuple(name for pair in config.LONG_SHORT_PAIRS for name in pair)


def load_bars(arctic_symbol: str, timeframe: str) -> pd.DataFrame:
"""Stored 4h bars (close-stamped), resampled to ``timeframe`` if needed."""
bars = data.load(arctic_symbol)
minutes = config.TIMEFRAMES[timeframe]
return bars if minutes == 240 else data.resample_closed(bars, minutes)


def exposure_job(
arctic_symbol: str, timeframe: str, names: tuple[str, ...]
) -> tuple[str, str, pd.DataFrame, dict[str, pd.Series]]:
bars = load_bars(arctic_symbol, timeframe)
if len(bars) < 2 * max(config.SLOW_SPANS):
return arctic_symbol, timeframe, bars, {}
inputs = filters.regime_inputs(bars)
exposures = {
name: ensemble.ensemble_exposure(
variants.run_variant(bars, variants.BY_NAME[name], inputs), bars.index
)
for name in names
}
return arctic_symbol, timeframe, bars, exposures


def run_exposures(
jobs: list[tuple[str, str]],
names: tuple[str, ...] = ALL_BOOKS,
workers: int = config.STUDY_WORKERS,
) -> dict[tuple[str, str], tuple[pd.DataFrame, dict[str, pd.Series]]]:
"""``{(arctic_symbol, timeframe): (bars, {variant: exposure})}`` for every job."""
if not jobs:
return {}
syms, tfs = zip(*jobs, strict=True)
with ProcessPoolExecutor(workers, mp_context=mp.get_context("spawn")) as pool:
results = list(pool.map(exposure_job, syms, tfs, [names] * len(jobs)))
return {(s, tf): (bars, exp) for s, tf, bars, exp in results}


def sized(bars: pd.DataFrame, exposure: pd.Series, sizing: str, timeframe: str) -> pd.Series:
if sizing == "raw":
return exposure
return exposure * ensemble.vol_weight(bars["close"], config.TIMEFRAMES[timeframe])


def returns(bars: pd.DataFrame, exposure: pd.Series) -> pd.Series:
return history.book_returns(bars, exposure, config.COST_BPS, config.EXECUTION)


def score(r: pd.Series, ann: float) -> dict[str, float]:
years = len(r) / ann**2
total = float((1 + r).prod())
return {
"sharpe": sweep.sharpe(r, ann),
"cagr": total ** (1 / years) - 1 if total > 0 and years > 0 else -1.0,
"max_drawdown": sweep.max_drawdown(r),
}


def annualization(timeframe: str) -> float:
return config.annualization(config.TIMEFRAMES[timeframe])


def span_mask(
index: pd.DatetimeIndex,
start: str | pd.Timestamp | None,
end: str | pd.Timestamp | None,
warmup_days: int = 120,
) -> pd.Series:
"""Bars inside [start, end), never earlier than ``warmup_days`` after the series begins."""
lo = index[0] + pd.Timedelta(days=warmup_days)
if start is not None:
lo = max(lo, pd.Timestamp(start, tz="UTC"))
hi = pd.Timestamp(end, tz="UTC") if end is not None else index[-1] + pd.Timedelta(seconds=1)
return pd.Series((index >= lo) & (index < hi), index=index)
Loading
Loading