- Add get_board_change_ranking() to MacClient and AsyncMacClient
- Add 'board-change-ranking' CLI command (--type/--date/--days/--top/--asc)
- Calculate N-day price change from board index K-lines directly
- Default to listing all boards; --top N to truncate
- 12 unit tests covering calculation, edges, sorting
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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
Use 'row_any: Any = row' pattern to avoid arg-type mismatch between
local (pandas-stubs) and CI (bare pandas) environments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- pyproject.toml: add mypy overrides for pandas/tabulate/matplotlib stubs,
disable strict checking for vendored MyTT library
- config.py: use cast() for dict[str, Any] .get() returns
- beichi.py: widen _calc_bi_force param to BI | XD, import XD
- backtest/cli.py: split combo/single strategy into separate typed variables
- backtest/combo.py: add bool_array() helper for numpy return types
- chanlun/analyser.py: type ignore for pandas row access, fix dict type arg
- unified.py: change fields param from object to Any
- ex/mac_client.py: add type args to list literals
- cli/cmd_offline.py: wrap int market as Market enum before API call
- cli/cmd_chanlun.py: fix dict type arg
- offline/write_*.py: explicit int() cast for struct.unpack returns
- MyTT.py: fix line-too-long comments, UP038 isinstance syntax
- tests: fix E712 (==False → ~mask), E741 (noqa), F841, import sorting
- ruff format applied across codebase
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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 --show/--show-chart flag to run_all_strategies.py
- Display dual-axis chart: normalized stock price vs strategy equity curve
- Mark buy/sell points with green/orange triangles
- Auto-detect Chinese fonts (SimHei/YaHei on Windows, PingFang on macOS)
- Fetch stock name via get_stock_quotes for chart title
- Add 3 demo screenshots to README with disclaimer
- Update README with --show usage and visual examples
- Consolidate version to pyproject.toml as single source of truth
- __init__.py, cli/__init__.py, docs/conf.py all read dynamically
- run_all_strategies.py now shows best strategy full trade details
- Update README changelog for 1.8.1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Performance dict outputs 'sharpe' but _print_table looked up 'sharpe_ratio',
so perf.get('sharpe_ratio', 0) always returned the default 0 regardless of
actual Sharpe value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previous formula was: max(absolute_drawdown) / initial_capital, which
exceeds 100% when the portfolio grows then drops (e.g. from 600k to 300k
on a 100k initial = 300% drawdown, which is nonsensical).
Fixed to use drawdown_pct (drawdown / peak) which is always in [0, 1].
This correctly measures the maximum percentage drop from the highest
equity peak, matching the standard financial definition.
Also added regression test: test_max_drawdown_never_exceeds_100_pct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MyTT.BIAS returns (BIAS6, BIAS12, BIAS24) but the strategy was assigning
all three to a single variable, causing 'array with more than one element'
ValueError when comparing to a scalar threshold.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: _generate_signals() iterated all bars calling strategy.next()
but never updated _position_size or _cash on the strategy. Strategies
that check self.position['size'] before buy/sell always saw 0, producing
only BUY signals with no SELL — exhausting cash and producing drawdowns
exceeding 100%.
Fix: add _update_strategy_position() that estimates position changes
after each bar's signals using close price. This gives the strategy an
accurate view of its holdings so it can correctly alternate buy/sell.
Regression tests added:
- test_position_aware_buy_sell_alternation: verifies BUY/SELL alternation
- test_position_aware_no_duplicate_buys: no suspicious tiny duplicate buys
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add dsl_strategy decorator in dsl.py (P1 skeleton implementation)
- Update __init__.py to export BacktestEngine, Strategy, and related types
- All 106 backtest unit tests pass
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Implement BacktestEngine orchestrator with 4-step pipeline:
1. Signal generation (Strategy)
2. Order simulation (OrderSimulator)
3. Portfolio tracking (PortfolioTracker)
4. Performance analysis (PerformanceAnalyzer)
- Support both strategy class and instance initialization
- Add PnL calculation for sell trades
- Add JSON serialization with numpy/timestamp support
- Include comprehensive test coverage (12 tests, all passing)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Implement PerformanceAnalyzer class with compute() method
- Calculate 19 performance metrics: total_return, annual_return, max_drawdown,
max_dd_duration, sharpe, sortino, calmar, trade statistics, and volatility
- Handle edge cases: empty data, no negative returns (sortino=999), no drawdown (calmar=999)
- Add 20 comprehensive unit tests covering all metrics
- Type annotations use NDArray pattern for mypy strict compliance
- All tests pass, mypy and ruff checks clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pre-allocate numpy arrays for performance (cash, position, avg_price)
- apply_trades() processes buys/sells with commission and slippage
- equity_curve returns DataFrame with drawdown calculation
- positions returns DataFrame with market value and unrealized PnL
- 12 unit tests covering all scenarios
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Implement OrderSimulator class for order matching simulation
- Support 5 execution modes: next_open, next_close, this_close, worst, best
- Support 3 position modes: full, fixed, percent
- Support 2 reject policies: reduce (partial fill), skip (reject)
- Implement fee model: commission (min 5 CNY), stamp tax (0.1% sell only), slippage
- Add future_leak_warning flag for this_close mode
- Handle both int and datetime column types in DataFrame
- Add comprehensive test suite with 24 test cases covering all modes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>