Root cause: _market_from_str/_market/_category in routers used bare
MarketEnum[key]/Market[key] without .upper() or try/except, so
lowercase or invalid values (sz, ZZZ) threw uncaught KeyError → 500.
Fix: extract shared convert.py with market_from_str/category_from_str
that do .upper() + ValueError on invalid input. All 4 routers updated.
4 regression tests added for case-insensitive and invalid input.
The typing.cast(AsyncTdxClient, ...) evaluated AsyncTdxClient at
runtime, but the import was gated behind TYPE_CHECKING, causing
NameError in production. Direct import is safe here — web module
already depends on easy_tdx core.
- Add EventBus for async publish/subscribe market events
- Add MarketEvent dataclass with tick/bar/signal/error types
- Add RealtimeStrategy base class with on_tick/on_bar callbacks
- Add emit_signal() for strategy-to-engine signal publishing
- Support per-symbol and global subscriptions
- API skeleton: transport-level subscription TBD
- Add 10 tests covering events, bus, and strategy
- Create type stub file for 50+ MyTT indicator functions
- Covers all 31 functions used in the project + common utilities
- Remove mypy disallow_untyped_defs/calls override for MyTT
- MyTT now covered by strict mypy via .pyi stubs
- Store previous DataFrame in ChanlunAnalyser after process_klines
- Add append_klines(df_new) to concatenate and recompute
- Handles datetime deduplication automatically
- Raises RuntimeError if called before initial process_klines
- Add 2 tests: append + recompute, error without init
- Add workers param to SignalScanner.scan() (default=0 for serial)
- workers=2+ uses ProcessPoolExecutor for parallel .day file processing
- Extract _scan_one_file as top-level function for pickle compatibility
- Results identical between serial and parallel modes
- Add 4 tests with synthetic .day file fixtures
- Add chanlun_level param to BacktestEngine constructor
- When set, auto-create ChanlunAnalyser and compute ChanlunResult
- Manual chanlun_result in run() takes priority over auto-compute
- Update Strategy.chanlun type to Any (accepts ChanlunResult or dict)
- Add 2 tests: auto-bridge and manual override priority
- Track SL/TP conditions from BUY signals in _generate_signals loop
- Check active conditions against each bar's high/low price range
- Auto-generate SELL signal at trigger price when condition is met
- Modify OrderSimulator to respect signal.price for direct execution
(previously signal.price was stored but never used in execution)
- SL/TP activates on bar AFTER BUY signal (consistent with next_open)
- Stop-loss checked before take-profit (conservative for holder)
- Add 4 tests: SL trigger, TP trigger, no-trigger, priority over manual sell
- Add --cov and --cov-fail-under=50 to CI pytest command
- Replace hardcoded avg_holding_days=5.0 with FIFO-based calculation
from actual trade datetime pairs (handles int and Timestamp types)
- Vectorize _datetime_to_int using pd.to_datetime().strftime()
instead of Python for-loop (~100-200x faster on large arrays)
- Add 3 new test cases: weighted holding days, no datetime fallback,
only-buys edge case
- Add 'screen' CLI command group with 'scan' and 'rank' subcommands
- scan: offline signal scanning from local .day files, zero network IO
- rank: backtest ranking of scanned signals by sharpe/drawdown/etc
- Two-step workflow: scan outputs JSON, rank reads JSON and evaluates
- Support --universe (all/sh/sz/custom file), --sort, --names
- Support pipe mode: scan ... | rank --from - --table
- New module: src/easy_tdx/screen/{scanner,ranker,cli}.py
- 20 unit tests (offline, no network required)
- screen() now calls run_combination() internally, eliminating duplicated
signal extraction/combination logic
- _run_combo_screen creates one CombinationRunner before the size loop,
so signal cache is reused across 2-factor and 3-factor screens
- Add MAJORITY(2)=AND note to screen() docstring
- Add .readthedocs.yaml build config (Ubuntu 22.04, Python 3.11)
- Add docs/conf.py with myst-parser for Markdown support
- Add docs/index.md toctree including README and existing docs
- Add docs/readme.md to include root README via myst directive
- Add docs/requirements.txt for Sphinx build dependencies
- Add docs/_build/ to .gitignore
Fix find_bis() greedy algorithm terminating early when dense alternating
fractals cause gap=0 for every opposite-type fractal. The root cause was
blindly replacing start_fx with more extreme same-type fractals, pushing
right_kline_index forward and making subsequent gaps permanently 0.
Solution: add pending_opposite guard — when an opposite-type fractal fails
the gap check, freeze start_fx replacement until a valid bi is formed.
- Affects: sustained up/down trends with dense fractals (e.g. high-price stocks)
- 600519: 114 bi (ending 04-28) -> 142 bi (ending 05-27)
- 601088: 131 bi -> 147 bi (end date unchanged)
- New regression test: test_fractal_trap_regression
- Bump version to 1.7.1
Root cause: _fetch_all_daily_bars used get_security_bars() for all files,
but index server responses have 4 extra bytes per record. Wrong parser
produced garbage dates like '12897-50-77' for sh000001, sh000300, etc.
Fix: add _is_index_code() to detect index codes by prefix (sh: 00/88/99,
sz: 39) and route to get_index_bars() accordingly.
Bumps version to 1.6.1.
- New 'offline' command group with 8 subcommands: home, daily, min,
ex-files, ex-daily, gbbq, financial, blocks
- No network required, reads local TDX data files directly
- Updated CLI examples and README with offline documentation
- Added v1.5.0 changelog entry
- K-line: daily+ periods output 'date' only, minute periods output 'datetime'
- Transactions (tick-by-tick): combine date param + hour/minute into 'datetime'
- XdxrRecord, HistoricalFundFlow: year/month/day merged to 'date'
- MinuteBar: rename unknown_1 to _unknown_1 (hidden from DataFrame)
- MinuteBar: add datetime column computed from bar index (A-share 240-bar pattern)
- get_minute_time_data: use history endpoint only (current-day endpoint broken in pytdx too)
- Update all examples to reflect new DataFrame column names
- K-line: daily+ periods output 'date' only, minute periods output 'datetime'
- Transactions (tick-by-tick): combine date param + hour/minute into 'datetime'
- XdxrRecord, HistoricalFundFlow: year/month/day merged to 'date'
- MinuteBar: rename unknown_1 to _unknown_1 (hidden from DataFrame)
- MinuteBar: add datetime column computed from bar index (A-share 240-bar pattern)
- get_minute_time_data: use history endpoint only (current-day endpoint broken in pytdx too)
- Update all examples to reflect new DataFrame column names