Skip to content

Commit 20a431c

Browse files
authored
Merge pull request #6 from StuartFarmer/position-backtester
Position backtester
2 parents 203a42b + 9903135 commit 20a431c

11 files changed

+7690
-1
lines changed

.coverage

52 KB
Binary file not shown.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ __pycache__/
1414
*.pyd
1515
.Python
1616

17-
.DS_Store
17+
.DS_Store
18+
19+
# MkDocs generated site
20+
site/
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
# Position-Based Backtester - Final Decisions
2+
3+
**Date**: 2025-10-23
4+
**Status**: Decisions finalized, ready for implementation
5+
6+
---
7+
8+
## Design Decisions (Finalized)
9+
10+
### Decision 1: Strategy Position Access
11+
**Choice**: Strategies can access positions via **alternative data loader** (existing pattern)
12+
13+
**Implication**:
14+
- **NO NEW `PositionStrategyBase` CLASS NEEDED**
15+
- Use existing `StrategyBase`
16+
- Strategies output same `Dict[str, float]` format
17+
- Backtester interprets values as **shares** instead of **weights**
18+
- Strategies wanting position info use alternative data (e.g., `daily_data['POSITIONS:AAPL']`)
19+
20+
**Example**:
21+
```python
22+
class MyStrategy(StrategyBase): # Use existing base class!
23+
def step(self, current_date, daily_data):
24+
# Access position via alternative data if needed
25+
# current_pos = daily_data.get('POSITIONS:AAPL', {}).get('shares', 0)
26+
27+
# Return share quantities (not weights!)
28+
return {'AAPL': 10, 'MSFT': -5} # Buy 10 AAPL, sell 5 MSFT
29+
```
30+
31+
---
32+
33+
### Decision 2: Execution Price Model
34+
**Choice**: Execute on **close** (same as weights backtester)
35+
36+
**Note**: Check if original backtester has open/close option. If it does, replicate that.
37+
38+
**Action Item**: Verify if `Backtester` supports execution price configuration
39+
- [ ] Check current `Backtester` for execution price parameter
40+
- [ ] If exists, replicate same parameter in `PositionBacktester`
41+
- [ ] If not, default to close only for MVP
42+
43+
---
44+
45+
### Decision 3: Cost Basis Method
46+
**Choice**: **Defer to external analyzer** - not calculated in core backtester
47+
48+
**Implication**:
49+
- Core backtester does NOT track cost basis
50+
- Core backtester does NOT calculate realized vs unrealized P&L
51+
- Backtester only tracks: positions, actions, prices, total value
52+
- **Analyzer** can calculate cost basis, realized P&L post-hoc using any method (average, FIFO, LIFO, etc.)
53+
54+
**Simplification**: This significantly reduces backtester complexity!
55+
56+
---
57+
58+
### Decision 4: Short Selling
59+
**Choice**: **Yes** - allow negative positions
60+
61+
**Implication**: No validation preventing negative positions
62+
63+
---
64+
65+
### Decision 5: Fractional Shares
66+
**Choice**: **Yes** - allow fractional shares
67+
68+
**Implication**: Use `float` for position quantities, not `int`
69+
70+
---
71+
72+
### Decision 6: Benchmark Comparison
73+
**Choice**: Follow existing backtester pattern (extensible functions)
74+
75+
**Default Approach**:
76+
- Benchmark holds position size of 1 per ticker, OR
77+
- Benchmark holds average notional position of strategy over time
78+
79+
**Implication**: Same benchmark architecture as weights backtester
80+
- Support benchmark functions
81+
- Allow custom callables
82+
- Defer sophisticated comparison to analyzers
83+
84+
---
85+
86+
### Decision 7: Initial Positions
87+
**Choice**: **No** - always start with empty portfolio
88+
89+
**Implication**: All backtests start with zero positions
90+
91+
---
92+
93+
### Decision 8: Universe Exit Handling
94+
**Choice**: **Follow weights backtester behavior**
95+
96+
**Action Item**: Verify what weights backtester does
97+
- [ ] Check `Backtester.run_backtest()` for universe exit handling
98+
- [ ] Replicate same logic in `PositionBacktester`
99+
100+
**Hypothesis**: Likely force-sells positions when ticker exits universe
101+
102+
---
103+
104+
### Decision 9: File Structure
105+
**Clarification**: No new strategy files needed (using existing `StrategyBase`)
106+
107+
**New Files Required**:
108+
```
109+
portwine/backtester/position_core.py # PositionBacktester only
110+
tests/test_position_backtester.py # Tests
111+
```
112+
113+
**No new files in**:
114+
- `strategies/` - Use existing `StrategyBase`
115+
- `analyzers/` - Defer to later phase
116+
117+
---
118+
119+
### Decision 10: Data Requirements
120+
**Choice**: Use **exact same data interface** as weights backtester
121+
122+
**Implication**:
123+
- Same `DataInterface`, `MultiDataInterface`, `RestrictedDataInterface`
124+
- Same price access patterns
125+
- If weights backtester uses close only, position backtester uses close only
126+
- If weights backtester supports OHLCV, position backtester gets it automatically
127+
128+
---
129+
130+
### Decision 11: Testing Approach
131+
**Choice**: **Hybrid** (write feature, test it, build next feature on top)
132+
133+
---
134+
135+
### Decision 12: MVP Feature Set
136+
**Choice**: Start with **minimal working backtester**, iterate rapidly
137+
138+
**MVP Definition**:
139+
- `PositionBacktester` class with `run_backtest()` method
140+
- Interprets strategy output as share quantities
141+
- Tracks positions over time
142+
- Tracks actions over time
143+
- Tracks prices over time
144+
- Calculates portfolio value (sum of position × price)
145+
- Returns results dict with DataFrames
146+
- NO cost basis tracking (defer to analyzer)
147+
- NO realized/unrealized split (defer to analyzer)
148+
- NO benchmarks initially (add if easy)
149+
150+
---
151+
152+
### Decision 13: Numba Optimization Timing
153+
**Choice**: **Python first**, optimize later
154+
155+
**Implication**: Get correctness first, add Numba JIT after tests pass
156+
157+
---
158+
159+
## Revised Architecture
160+
161+
### Core Insight: Much Simpler Than Original Design!
162+
163+
**Original Plan** (too complex):
164+
- New `PositionStrategyBase` class
165+
- Complex cost basis tracking in backtester
166+
- Realized vs unrealized P&L split
167+
- Position injection into strategy
168+
169+
**Revised Plan** (your approach):
170+
- Use existing `StrategyBase`
171+
- No cost basis in backtester (defer to analyzer) ✓
172+
- Simple portfolio value calculation ✓
173+
- Position access via alternative data (existing pattern) ✓
174+
175+
---
176+
177+
## Simplified Position Backtester Architecture
178+
179+
### Input
180+
```python
181+
strategy = StrategyBase(['AAPL', 'MSFT']) # Existing class!
182+
183+
# Strategy returns shares (backtester interprets as quantities)
184+
def step(self, current_date, daily_data):
185+
return {'AAPL': 10} # Buy 10 shares
186+
```
187+
188+
### Processing
189+
```python
190+
# PositionBacktester.run_backtest()
191+
for date in datetime_index:
192+
actions = strategy.step(date, data) # {'AAPL': 10}
193+
194+
# Update positions
195+
for ticker, action in actions.items():
196+
positions[ticker] += action
197+
198+
# Record state
199+
record_positions(date, positions)
200+
record_actions(date, actions)
201+
record_prices(date, current_prices)
202+
203+
# Calculate portfolio value
204+
portfolio_value = sum(positions[t] * prices[t] for t in tickers)
205+
```
206+
207+
### Output
208+
```python
209+
results = {
210+
'positions_df': DataFrame, # (days × tickers) share quantities
211+
'actions_df': DataFrame, # (days × tickers) buy/sell quantities
212+
'prices_df': DataFrame, # (days × tickers) execution prices
213+
'portfolio_value': Series, # (days,) total portfolio value
214+
'benchmark_returns': Series # (days,) if benchmark provided
215+
}
216+
```
217+
218+
### Post-Processing (Analyzers)
219+
```python
220+
# Analyzer calculates cost basis, realized P&L, etc.
221+
# Using whatever method desired (average, FIFO, LIFO)
222+
223+
analyzer = PositionCostBasisAnalyzer(method='average')
224+
pnl = analyzer.analyze(results)
225+
# Returns: realized_pnl, unrealized_pnl, cost_basis_history, etc.
226+
```
227+
228+
---
229+
230+
## Simplified Output API
231+
232+
### Minimal Output (MVP)
233+
```python
234+
{
235+
'positions_df': pd.DataFrame, # Share positions over time
236+
'actions_df': pd.DataFrame, # Buy/sell actions over time
237+
'prices_df': pd.DataFrame, # Execution prices
238+
'portfolio_value': pd.Series, # Total value (Σ position × price)
239+
}
240+
```
241+
242+
### With Benchmark (MVP+)
243+
```python
244+
{
245+
# ... everything above, plus:
246+
'benchmark_returns': pd.Series, # Or benchmark_portfolio_value?
247+
}
248+
```
249+
250+
**Note**: No realized/unrealized split, no cost basis - that's analyzer territory!
251+
252+
---
253+
254+
## Key Questions to Verify Before Implementation
255+
256+
### Question 1: Weights Backtester Execution Price
257+
**Need to check**: Does current `Backtester` have execution price configuration?
258+
259+
**File to check**: `portwine/backtester/core.py` - look for parameters in `run_backtest()`
260+
261+
**Expected**: Likely just uses close, maybe has option for next open
262+
263+
---
264+
265+
### Question 2: Weights Backtester Universe Exit
266+
**Need to check**: What happens when ticker exits universe with open position?
267+
268+
**File to check**: `portwine/backtester/core.py` - look in main loop for universe changes
269+
270+
**Expected**: Likely force-liquidates or raises warning
271+
272+
---
273+
274+
### Question 3: Strategy Output Format
275+
**Need to verify**: Current strategies return `Dict[str, float]` for weights
276+
277+
**Confirmation**: Position strategies will return same format, just interpreted as shares
278+
279+
**Validation**: Should we add any new validation besides "ticker in universe"?
280+
281+
---
282+
283+
## Implementation Simplifications
284+
285+
### Removed from Original Plan:
286+
1.`PositionStrategyBase` class (use existing `StrategyBase`)
287+
2. ❌ Cost basis tracking in backtester (defer to analyzer)
288+
3. ❌ Realized vs unrealized P&L calculation (defer to analyzer)
289+
4. ❌ Position injection into strategy (use alternative data)
290+
5. ❌ Complex benchmark conversion (use simple approach)
291+
6. ❌ Initial positions support (not needed)
292+
7. ❌ New data interfaces (use existing)
293+
294+
### Kept in Plan:
295+
1.`PositionBacktester` class (new file)
296+
2. ✅ Position tracking (core feature)
297+
3. ✅ Action tracking (core feature)
298+
4. ✅ Price tracking (core feature)
299+
5. ✅ Portfolio value calculation (simple sum)
300+
6. ✅ Results dict with DataFrames (match existing API)
301+
7. ✅ Benchmark support (same extensible pattern)
302+
303+
---
304+
305+
## Estimated Complexity Reduction
306+
307+
**Original Estimate**: 5-6 weeks, 2000+ lines of code
308+
309+
**Revised Estimate**: 2-3 weeks, 500-800 lines of code
310+
311+
**Reason**:
312+
- No new strategy base class (~200 lines saved)
313+
- No cost basis tracking (~300 lines saved)
314+
- No realized/unrealized split (~200 lines saved)
315+
- Reuse existing data interfaces (~400 lines saved)
316+
- Simpler validation (~100 lines saved)
317+
318+
---
319+
320+
## Next Steps
321+
322+
1. ✅ Decisions finalized
323+
2. ⏳ Verify weights backtester behavior (execution price, universe exit)
324+
3. ⏳ Create iteration-by-iteration implementation plan
325+
4. ⏳ Begin implementation
326+
327+
---
328+
329+
## Implementation Philosophy
330+
331+
**Your approach** (lean, iterative):
332+
- Start with simplest possible working version
333+
- Test immediately
334+
- Add one feature at a time
335+
- Build on tested foundation
336+
- Defer complexity to analyzers
337+
338+
**This aligns perfectly with**:
339+
- Unix philosophy (do one thing well)
340+
- Separation of concerns (backtester ≠ analyzer)
341+
- Existing portwine architecture (analyzers consume backtest results)
342+
343+
---
344+
345+
## Summary
346+
347+
The position backtester is **much simpler** than originally designed:
348+
349+
1. **No new strategy class** - use existing `StrategyBase`
350+
2. **No cost basis** - defer to analyzers
351+
3. **Simple position tracking** - just accumulate actions
352+
4. **Simple portfolio value** - just sum position × price
353+
5. **Same data interfaces** - reuse everything
354+
6. **Same patterns** - execution, benchmarks, validation
355+
356+
This is a **weekend project**, not a month-long effort!

0 commit comments

Comments
 (0)