Skip to content

Commit 596a7bb

Browse files
committed
Add normalized position scaling
1 parent a1dcc7e commit 596a7bb

5 files changed

Lines changed: 765 additions & 47 deletions

File tree

docusaurus/docs/Getting Started/positions.md

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,14 @@ def apply_strategy(self, algorithm, market_data):
3939
for position in positions:
4040
print(f"Position: {position.symbol}")
4141
print(f"Amount: {position.amount}")
42-
print(f"Entry Price: {position.entry_price}")
43-
print(f"Current Value: {position.current_value}")
4442
```
4543

4644
### Get Specific Position
4745

4846
```python
4947
def apply_strategy(self, algorithm, market_data):
5048
# Get position for specific symbol
51-
btc_position = algorithm.get_position("BTC/USDT")
49+
btc_position = algorithm.get_position("BTC")
5250

5351
if btc_position:
5452
print(f"BTC Position: {btc_position.amount} BTC")
@@ -75,13 +73,14 @@ def analyze_position(self, position, market_data):
7573

7674
print(f"Symbol: {symbol}")
7775
print(f"Amount: {amount}")
78-
print(f"Entry: ${entry_price:.2f}")
79-
print(f"Current: ${current_price:.2f}")
80-
print(f"P&L: ${unrealized_pnl:.2f} ({pnl_percentage:.2f}%)")
8176
```
8277

8378
## Position Management Strategies
8479

80+
:::tip Multi-Symbol Strategy Allocation
81+
When trading multiple symbols simultaneously, ensure your total position size allocations don't exceed 100% of your portfolio. The framework will automatically scale orders proportionally if needed, but it's best practice to plan allocations that fit within available funds (e.g., 5 symbols × 20% = 100%).
82+
:::
83+
8584
### Profit Taking
8685

8786
```python
@@ -142,6 +141,58 @@ class StopLossStrategy(TradingStrategy):
142141

143142
### Position Sizing
144143

144+
The framework provides a `PositionSize` class to define how much capital to allocate to each symbol. You can specify either a percentage of portfolio or a fixed amount.
145+
146+
```python
147+
from investing_algorithm_framework import TradingStrategy, PositionSize, TimeUnit
148+
149+
class MultiAssetStrategy(TradingStrategy):
150+
time_unit = TimeUnit.HOUR
151+
interval = 2
152+
symbols = ["BTC", "ETH", "SOL", "ADA", "XRP"]
153+
154+
# Define position sizes for each symbol
155+
position_sizes = [
156+
PositionSize(symbol="BTC", percentage_of_portfolio=20.0),
157+
PositionSize(symbol="ETH", percentage_of_portfolio=20.0),
158+
PositionSize(symbol="SOL", percentage_of_portfolio=20.0),
159+
PositionSize(symbol="ADA", percentage_of_portfolio=20.0),
160+
PositionSize(symbol="XRP", percentage_of_portfolio=20.0),
161+
]
162+
```
163+
164+
#### Fixed Amount vs Percentage
165+
166+
```python
167+
# Percentage-based: allocates 20% of total portfolio value
168+
PositionSize(symbol="BTC", percentage_of_portfolio=20.0)
169+
170+
# Fixed amount: always allocates exactly 500 EUR
171+
PositionSize(symbol="ETH", fixed_amount=500.0)
172+
```
173+
174+
#### Proportional Scaling
175+
176+
When multiple buy signals fire simultaneously and the total requested allocation exceeds available funds, the framework **automatically scales all orders proportionally** to ensure fair allocation across all symbols.
177+
178+
**Example:**
179+
- Portfolio: 1000 EUR available
180+
- 5 symbols each want 20% = 200 EUR each = 1000 EUR total
181+
- If only 800 EUR is available, each order is scaled to 160 EUR (80% of requested)
182+
183+
This behavior ensures:
184+
- **Fair allocation**: All symbols get the same percentage reduction
185+
- **Predictable results**: Order of symbols doesn't affect allocation
186+
- **No failures**: Orders are scaled rather than rejected
187+
188+
```python
189+
# If total allocation exceeds available funds, you'll see a warning:
190+
# "Total allocation (1000.00) exceeds available funds (800.00).
191+
# Scaling all orders by 80.00% to maintain proportional allocation."
192+
```
193+
194+
#### Rebalancing Example
195+
145196
```python
146197
class PositionSizingStrategy(TradingStrategy):
147198

examples/tutorial/notebooks/01_data_exploration.ipynb

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,142 @@
229229
" file_path=str(file_path)\n",
230230
" )\n"
231231
]
232+
},
233+
{
234+
"cell_type": "markdown",
235+
"id": "11",
236+
"metadata": {},
237+
"source": [
238+
"## Analysis on the Backtest Windows"
239+
]
240+
},
241+
{
242+
"cell_type": "code",
243+
"execution_count": null,
244+
"id": "12",
245+
"metadata": {},
246+
"outputs": [],
247+
"source": [
248+
"import numpy as np\n",
249+
"from typing import Dict, Tuple\n",
250+
"import pandas as pd\n",
251+
"from investing_algorithm_framework import create_markdown_table, BacktestDateRange\n",
252+
"from IPython.display import Markdown, display\n",
253+
"\n",
254+
"\n",
255+
"def show_backtest_windows_analysis(\n",
256+
" data: Dict[str, Tuple[BacktestDateRange, pd.DataFrame]],\n",
257+
"):\n",
258+
" \"\"\"\n",
259+
" Show analysis of backtest windows. Each entry in `data` should map\n",
260+
" a label to a tuple of (date_range, ohlcv_dataframe).\n",
261+
"\n",
262+
" Args:\n",
263+
" data (Dict[str, Tuple[BacktestDateRange, pd.DataFrame]]): Mapping\n",
264+
" of labels (backtest window identifiers) to\n",
265+
" (date_range, ohlcv_dataframe)\n",
266+
"\n",
267+
" Returns:\n",
268+
" List[Dict]: List of detailed analysis dictionaries for each window\n",
269+
" \"\"\"\n",
270+
" summary_data = []\n",
271+
" detailed_analysis = []\n",
272+
"\n",
273+
" for key, (date_range, df) in data.items():\n",
274+
" sliced_data = df[date_range.start_date:date_range.end_date].copy()\n",
275+
"\n",
276+
" if sliced_data.empty:\n",
277+
" continue\n",
278+
"\n",
279+
" # Calculate comprehensive metrics\n",
280+
" sliced_data['returns'] = sliced_data['Close'].pct_change().dropna()\n",
281+
"\n",
282+
" start_price = sliced_data['Close'].iloc[0]\n",
283+
" end_price = sliced_data['Close'].iloc[-1]\n",
284+
" total_return = (end_price / start_price - 1) * 100\n",
285+
"\n",
286+
" daily_returns = sliced_data['returns'] * 100\n",
287+
" volatility = daily_returns.std() * np.sqrt(365)\n",
288+
" mean_daily_return = daily_returns.mean()\n",
289+
" sharpe_ratio = (mean_daily_return * 365) / volatility if volatility > 0 else 0\n",
290+
"\n",
291+
" # Drawdown analysis\n",
292+
" rolling_max = sliced_data['Close'].cummax()\n",
293+
" drawdown = (sliced_data['Close'] / rolling_max - 1) * 100\n",
294+
" max_drawdown = drawdown.min()\n",
295+
"\n",
296+
" # Volatility regimes\n",
297+
" high_vol_days = (daily_returns.abs() > daily_returns.abs().quantile(0.8)).sum()\n",
298+
" low_vol_days = (daily_returns.abs() < daily_returns.abs().quantile(0.2)).sum()\n",
299+
"\n",
300+
" # Trend analysis (count of data points, not calendar days)\n",
301+
" up_periods = (daily_returns > 0).sum()\n",
302+
" down_periods = (daily_returns < 0).sum()\n",
303+
" total_periods = len(sliced_data)\n",
304+
"\n",
305+
" # Duration in calendar days\n",
306+
" duration_days = (date_range.end_date - date_range.start_date).days\n",
307+
" start_date_str = date_range.start_date.strftime('%Y-%m-%d')\n",
308+
" end_date_str = date_range.end_date.strftime('%Y-%m-%d')\n",
309+
"\n",
310+
" summary_data.append({\n",
311+
" \"window\": key,\n",
312+
" \"date_range\": f\"{start_date_str} to {end_date_str}\",\n",
313+
" \"days\": str(duration_days),\n",
314+
" \"avg_daily_return\": f\"{mean_daily_return:.3f}%\",\n",
315+
" \"cumulative_return\": f\"{total_return:.2f}%\",\n",
316+
" \"volatility_ann\": f\"{volatility:.2f}%\",\n",
317+
" \"sharpe_ratio\": f\"{sharpe_ratio:.2f}\",\n",
318+
" \"max_drawdown\": f\"{max_drawdown:.2f}%\",\n",
319+
" \"up_periods\": f\"{up_periods} ({up_periods/total_periods*100:.1f}%)\",\n",
320+
" \"down_periods\": f\"{down_periods} ({down_periods/total_periods*100:.1f}%)\",\n",
321+
" \"high_vol_periods\": f\"{high_vol_days} ({high_vol_days/total_periods*100:.1f}%)\",\n",
322+
" \"low_vol_periods\": f\"{low_vol_days} ({low_vol_days/total_periods*100:.1f}%)\"\n",
323+
" })\n",
324+
"\n",
325+
" # Detailed analysis for each period\n",
326+
" detailed_analysis.append({\n",
327+
" 'name': key,\n",
328+
" 'total_return': total_return,\n",
329+
" 'volatility': volatility,\n",
330+
" 'sharpe_ratio': sharpe_ratio,\n",
331+
" 'max_drawdown': max_drawdown,\n",
332+
" 'up_periods': up_periods,\n",
333+
" 'down_periods': down_periods,\n",
334+
" 'high_vol_periods': high_vol_days,\n",
335+
" 'low_vol_periods': low_vol_days,\n",
336+
" 'duration_days': duration_days,\n",
337+
" 'total_periods': total_periods,\n",
338+
" 'mean_daily_return': mean_daily_return,\n",
339+
" 'start_price': start_price,\n",
340+
" 'end_price': end_price\n",
341+
" })\n",
342+
"\n",
343+
" # Create and display the markdown table\n",
344+
" table = create_markdown_table(summary_data)\n",
345+
" display(Markdown(table))\n",
346+
"\n",
347+
" return detailed_analysis\n",
348+
"\n",
349+
"\n",
350+
"# Prepare data for analysis - use BTC as reference asset\n",
351+
"btc_data = in_sample_data[\"BTC\"][\"2h\"][\"data\"]\n",
352+
"\n",
353+
"# Create analysis data dictionary from rolling backtest windows\n",
354+
"analysis_data = {}\n",
355+
"for i, window in enumerate(rolling_backtest_windows):\n",
356+
" train_range = window[\"train_range\"]\n",
357+
" analysis_data[f\"Window {i+1} (Train)\"] = (train_range, btc_data)\n",
358+
"\n",
359+
" if \"test_range\" in window:\n",
360+
" test_range = window[\"test_range\"]\n",
361+
" analysis_data[f\"Window {i+1} (Test)\"] = (test_range, btc_data)\n",
362+
"\n",
363+
"# Show the analysis\n",
364+
"detailed_results = show_backtest_windows_analysis(\n",
365+
" data=analysis_data,\n",
366+
")\n"
367+
]
232368
}
233369
],
234370
"metadata": {

investing_algorithm_framework/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .analysis import generate_rolling_backtest_windows, \
22
select_backtest_date_ranges, rank_results, create_weights, \
3-
get_missing_timeseries_data_entries, fill_missing_timeseries_data
3+
get_missing_timeseries_data_entries, fill_missing_timeseries_data, \
4+
create_markdown_table
45
from .app import App, Algorithm, \
56
TradingStrategy, StatelessAction, Task, AppHook, Context, \
67
add_html_report, BacktestReport, \
@@ -205,5 +206,6 @@
205206
"generate_rolling_backtest_windows",
206207
"tqdm",
207208
"get_missing_timeseries_data_entries",
208-
"fill_missing_timeseries_data"
209+
"fill_missing_timeseries_data",
210+
"create_markdown_table"
209211
]

0 commit comments

Comments
 (0)