Skip to content

feat(pullback): univers U1, stockage journalier et règles pullback (phases 1-2, F-43/F-44/F-45) - #111

Open
laurentvv wants to merge 2 commits into
docs/plan-migration-pullbackfrom
feat/pullback-universe-data
Open

laurentvv wants to merge 2 commits into
docs/plan-migration-pullbackfrom
feat/pullback-universe-data

Conversation

@laurentvv

@laurentvv laurentvv commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Résumé

Phases 1 et 2 du plan docs/PLAN_MIGRATION_PULLBACK_2026-10-12.md, empilées sur #110 (base = docs/plan-migration-pullback, à rebaser sur main après le merge de #110).

Phase 1 : univers et données

  • src/pullback/universe.py : 6 877 symboles Yahoo issus des _US_EQ en USD de T212 (symbole = shortName), filtre de liquidité sans regard vers le futur.
  • src/pullback/store.py : Parquet par symbole dans data_cache/pullback/ (gitignoré), barres manquantes seulement, séries périmées refusées, aucune donnée comblée.
  • src/pullback/cli.py : universe, update, stats.

Phase 2 : règles en fonctions pures (src/pullback/rules.py)

  • screener momentum, consolidation, support (EMA 9/21/50, AVWAP ancré, anciens plus hauts, gaps), jour de setup, taille (0,5 % de risque, plafonds 25 % / 100 % / 3 %), cibles 3R/5R, sortie sous EMA 9.
  • Test d'absence de regard vers le futur (contrat 13).
  • Fréquence réelle 2021-2026 sans filtre momentum : AAPL 17 setups, NVDA 10, PLTR 1.
  • Coût ~6 ms par symbole-jour : le banc de la phase 3 devra pré-filtrer par momentum ou vectoriser.

Suite complète : 451 passed, 3 skipped. Ne touche ni src/t212_executor.py ni schedule.py (contrat, critère 28).

À noter

  • Yahoo répond « possibly delisted » pour une partie des instruments encore listés par T212 : ils sont absents du jour. La couverture réelle sera reportée en commentaire après le téléchargement complet.

🤖 Generated with Claude Code

laurentvv and others added 2 commits October 12, 2026 21:13
…alier incrémental (phase 1)

- src/pullback/universe.py : T212 -> symbole Yahoo (shortName, BRK.B -> BRK-B), filtre de
  liquidité (cours >= 5 $, 20 M$/jour, 252 séances) sans regard vers le futur.
- src/pullback/store.py : Parquet par symbole, seules les barres manquantes sont téléchargées,
  séries périmées (> 3 jours ouvrés) refusées, aucune donnée comblée.
- src/pullback/cli.py : universe / update / stats.
- 10 tests ; suite complète 442 passed, 3 skipped. N'affecte ni t212_executor.py ni schedule.py.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Screener momentum (+30 %, rang >= 75), détection de consolidation (creux ascendants, ATR5/ATR20,
EMA 9/21), niveaux de support (EMA, AVWAP ancré, anciens plus hauts, gaps non comblés), jour de
setup (déclencheur = plus haut, stop = plus bas, distance <= 3 %), taille (0,5 % de risque, plafonds
25 % / 100 % / 3 %), cibles 3R/5R et sortie sous EMA 9. Aucune lecture après `asof` (testé).

Corrige aussi cli.py : le symbole « NAN » était lu comme NaN par pandas et plantait le téléchargement.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@laurentvv laurentvv changed the title feat(pullback): univers U1 et stockage journalier (phase 1, F-43/F-44) feat(pullback): univers U1, stockage journalier et règles pullback (phases 1-2, F-43/F-44/F-45) Oct 12, 2026
Comment thread src/pullback/rules.py
close = b["Close"]
e9, e21, e50 = ema(close, 9), ema(close, 21), ema(close, 50)
window = b.iloc[-p.consolidation_max_bars :]
anchor = len(b) - len(window) + int(np.argmax(window["High"].to_numpy()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Anchor calculation uses highest high within consolidation window

The anchor for AVWAP should be the peak before consolidation starts. np.argmax(window["High"]) finds the highest high anywhere in the last 30 bars (the consolidation window), which could be at the end of the window if there's upward drift during consolidation. This misplaces the AVWAP anchor.

Consider finding the peak before the consolidation window, or using the first bar of the window as the anchor (since the window is defined as the consolidation period).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/rules.py
if bars_since < p.consolidation_min_bars:
reasons.append("plus haut trop récent (pas de consolidation)")
lows = [i for i in pivot_lows(b) if i > anchor]
if len(lows) < 2 or not b["Low"].iloc[lows[-1]] > b["Low"].iloc[lows[-2]]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Only checks last two pivot lows for ascending pattern

The comment says "creux ascendants" (ascending lows, plural) but the code only verifies the last two pivot lows: b["Low"].iloc[lows[-1]] > b["Low"].iloc[lows[-2]]. If there are 3+ pivot lows where early ones descend but the last two ascend, this passes incorrectly.

Should verify all consecutive pivot lows after anchor are ascending: all(b["Low"].iloc[lows[i]] > b["Low"].iloc[lows[i-1]] for i in range(1, len(lows))).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/cli.py
from dotenv import load_dotenv

load_dotenv(".env.t212")
from src.t212_executor import _get_t212_base_url, get_auth_header

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Imports private functions from t212_executor

from src.t212_executor import _get_t212_base_url, get_auth_header imports _get_t212_base_url (prefixed with _ indicating private). This creates tight coupling to internal implementation details. If t212_executor refactors these, the CLI breaks.

Consider exposing a public helper in t212_executor or duplicating the minimal logic needed here.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/store.py

def is_stale(last_bar: pd.Timestamp, asof: pd.Timestamp, max_days: int = MAX_STALE_BUSINESS_DAYS) -> bool:
"""Périmé si plus de `max_days` jours ouvrés séparent la dernière barre de `asof`."""
return int(np.busday_count(last_bar.date(), asof.date())) > max_days

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: is_stale ignores market holidays

np.busday_count only excludes weekends, not market holidays (e.g., Thanksgiving, Christmas). Around holidays, this can incorrectly mark fresh data as stale (or vice versa) by 1-2 days.

The project's AGENTS.md notes this pattern elsewhere (GO-gate 5: "stale-at-source price data is refused"). Consider using a market calendar (e.g., pandas_market_calendars) or maintaining a holiday list.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/store.py
for s in chunk:
_merge(s, got.get(s), "new")
if todo_inc:
start = (min(todo_inc.values()) + pd.Timedelta(days=1)).strftime("%Y-%m-%d")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Start date uses calendar days, not business days

start = (min(todo_inc.values()) + pd.Timedelta(days=1)).strftime("%Y-%m-%d") adds 1 calendar day. If the last bar was Friday, this requests from Saturday. yfinance handles this gracefully but it's imprecise. Use pd.offsets.BDay(1) for 1 business day.

Suggested change
start = (min(todo_inc.values()) + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
start = (min(todo_inc.values()) + pd.offsets.BDay(1)).strftime("%Y-%m-%d")

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/universe.py
rows = []
for ins in instruments:
ticker = ins.get("ticker", "")
if ins.get("type") != "STOCK" or not ticker.endswith(US_SUFFIX) or ins.get("currencyCode") != "USD":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Case-sensitive comparisons for API fields

ins.get("type") != "STOCK" and ins.get("currencyCode") != "USD" assume exact casing. The T212 API could return "stock", "Stock", "usd", etc. Use case-insensitive comparison: ins.get("type", "").upper() != "STOCK" and ins.get("currencyCode", "").upper() != "USD".


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/cli.py


def cmd_update(limit: int | None) -> int:
symbols = list(pd.read_csv(UNIVERSE_CSV, keep_default_na=False)["symbol"])[:limit]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: No error handling for missing universe CSV

pd.read_csv(UNIVERSE_CSV, keep_default_na=False) raises FileNotFoundError if the file doesn't exist (e.g., user runs update before universe). Add a try/except or check UNIVERSE_CSV.exists() first.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/cli.py


def cmd_stats() -> int:
symbols = list(pd.read_csv(UNIVERSE_CSV, keep_default_na=False)["symbol"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: No error handling for missing universe CSV

Same issue as line 45: pd.read_csv(UNIVERSE_CSV, keep_default_na=False) will raise FileNotFoundError if universe command hasn't been run first.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/store.py

kwargs = {"start": start} if start else {"period": period or INITIAL_PERIOD}
raw = yf.download(
symbols, interval="1d", auto_adjust=True, group_by="ticker", progress=False, threads=True, timeout=30, **kwargs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: 30s timeout may be too short for 100 symbols

yf.download(..., timeout=30, ...) with BATCH=100 and threads=True may time out on slow networks or when Yahoo rate-limits. Consider increasing to 60-120s or making it configurable.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/pullback/cli.py


def main(argv: list[str] | None = None) -> int:
logging.basicConfig(level=logging.WARNING)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: logging.basicConfig called on every main() invocation

If main() is called multiple times (e.g., in tests), this adds duplicate handlers. Guard with if not logging.getLogger().handlers: or configure logging once at module level.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 10 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 7
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
src/pullback/rules.py 115 Anchor calculation uses highest high within consolidation window instead of peak before consolidation
src/pullback/rules.py 121 Only checks last two pivot lows for ascending pattern
src/pullback/cli.py 29 Imports private functions from t212_executor
src/pullback/store.py 46 is_stale ignores market holidays (np.busday_count only excludes weekends)
src/pullback/universe.py 40 Case-sensitive comparisons for API fields (type, currencyCode)
src/pullback/cli.py 45 No error handling for missing universe CSV
src/pullback/cli.py 52 No error handling for missing universe CSV

SUGGESTION

File Line Issue
src/pullback/store.py 106 Start date uses calendar days instead of business days
src/pullback/store.py 122 30s timeout may be too short for 100-symbol batch download
src/pullback/cli.py 66 logging.basicConfig called on every main() invocation
Files Reviewed (9 files)
  • src/pullback/universe.py - 1 issue
  • src/pullback/store.py - 3 issues
  • src/pullback/rules.py - 2 issues
  • src/pullback/cli.py - 4 issues
  • tests/test_pullback_universe_store.py - No issues
  • tests/test_pullback_rules.py - No issues
  • memory-bank/feature_list.json - Status tracking only
  • memory-bank/log.md - Status tracking only
  • memory-bank/progress.md - Status tracking only

Fix these issues in Kilo Cloud


Reviewed by free · Input: 202.4K · Output: 10.1K · Cached: 267.8K

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant