Adaptive AI Hedge Fund Replication Strategy (Simulator)

Strategy Definition

Factors ("AI Signals") & Assets

Factor Definition

Asset Definition (Portfolio Components)

Adaptive Rule Builder (IF-THEN for Allocation Changes)

Rules determine target asset allocations based on factor conditions. The sum of target allocations in a rule's actions should be 100%.

Initial Allocation & Rebalancing Rules

Initial Target Asset Allocation (%)

Set the starting allocation for your portfolio. Must sum to 100%.

Total Initial Allocation: 0%

Rebalancing Rules

Simulation Data Input (Market & Factor Steps)

For each simulation step, provide observed factor values and the performance change (%) for each asset.

Simulation Results

Performance summary will appear here after running the simulation.

Step Factor Values Rule Triggered Old Alloc. (%) New Alloc. (%) Asset Perf. (%) Step P&L ($) Portfolio Value ($)

Define Factors and Assets in Tab 2 first.

'; return; } aahfrs_model.simulationData.forEach((step, index) => { const card = document.createElement('div'); card.className = 'aahfrs_dynamic_item_card'; let factorInputsHTML = '

Factor Values for this Step:

'; aahfrs_model.factors.forEach(f => { let inputHTML = ''; if (f.type === 'categorical') { const catOptions = (f.categories || []).map(c => ``).join(''); inputHTML = ``; } else { // numerical inputHTML = ``; } factorInputsHTML += `
${inputHTML}
`; }); factorInputsHTML += '
'; let assetPerfHTML = '

Asset Performance (%) for this Step:

'; aahfrs_model.assets.forEach(a => { assetPerfHTML += `
`; }); assetPerfHTML += '
'; card.innerHTML = `

Simulation Step ${index + 1}

${factorInputsHTML} ${assetPerfHTML} `; container.appendChild(card); }); } // --- Tab 6: Simulation & Results --- // This is the most complex part function aahfrs_runSimulation() { aahfrs_collectModelInputs(); // Ensure model object is up-to-date aahfrs_simulationLog = []; const logBody = aahfrs_getEl('aahfrs_simulationLogBody'); const summaryDiv = aahfrs_getEl('aahfrs_simulationSummary'); if (!logBody || !summaryDiv) return; logBody.innerHTML = ''; summaryDiv.innerHTML = '

Running simulation...

'; let currentPortfolioValue = aahfrs_model.initialCapital; let currentAllocations = { ...aahfrs_model.initialAllocationSet }; // Percentages let assetValues = {}; // Stores $ value of each asset aahfrs_model.assets.forEach(asset => { assetValues[asset.id] = currentPortfolioValue * ((currentAllocations[asset.id] || 0) / 100); }); let portfolioValueHistory = [currentPortfolioValue]; if(Object.values(currentAllocations).reduce((s,v)=>s+v,0) !== 100 && aahfrs_model.assets.length > 0){ summaryDiv.innerHTML = `

Error: Initial asset allocations do not sum to 100%. Please correct in 'Allocation' tab.

`; return; } aahfrs_model.simulationData.forEach((stepData, stepIndex) => { let stepPnl = 0; let ruleTriggeredName = "None"; let oldAllocationsForLog = {}; let newAllocationsForLog = {}; let assetPerfForLog = {}; // 1. Calculate portfolio value change based on asset performances let portfolioValueAfterPerformance = 0; for (const asset of aahfrs_model.assets) { const perfChange = (stepData.assetPerformance[asset.id] || 0) / 100; const valueChange = assetValues[asset.id] * perfChange; assetValues[asset.id] += valueChange; stepPnl += valueChange; portfolioValueAfterPerformance += assetValues[asset.id]; assetPerfForLog[asset.name || asset.id] = (stepData.assetPerformance[asset.id] || 0); } currentPortfolioValue = portfolioValueAfterPerformance; // Store current allocations (before adaptation) for logging aahfrs_model.assets.forEach(asset => { oldAllocationsForLog[asset.name || asset.id] = currentPortfolioValue > 0 ? (assetValues[asset.id] / currentPortfolioValue * 100) : 0; }); // 2. Evaluate current factor values against Adaptive Rules let newTargetAllocations = null; // This will hold the allocations from a triggered rule const currentStepFactorValues = stepData.factorValues; for (const rule of aahfrs_model.rules) { let conditionsMet = true; for (const cond of rule.conditions) { const factorValueInStep = currentStepFactorValues[cond.factorId]; const ruleValue = cond.value; const factorDef = aahfrs_model.factors.find(f => f.id === cond.factorId); let conditionHolds = false; if (factorDef && factorDef.type === 'numerical') { const numFactorValue = parseFloat(factorValueInStep); const numRuleValue = parseFloat(ruleValue); if (isNaN(numFactorValue) || isNaN(numRuleValue)) { conditionsMet = false; break; } switch(cond.operator) { case 'is': conditionHolds = numFactorValue === numRuleValue; break; case '>': conditionHolds = numFactorValue > numRuleValue; break; case '<': conditionHolds = numFactorValue < numRuleValue; break; case '>=': conditionHolds = numFactorValue >= numRuleValue; break; case '<=': conditionHolds = numFactorValue <= numRuleValue; break; default: conditionsMet = false; } } else { // Categorical conditionHolds = String(factorValueInStep).toLowerCase() === String(ruleValue).toLowerCase(); } if (!conditionHolds) { conditionsMet = false; break; } } if (conditionsMet) { ruleTriggeredName = `Rule ${rule.id.split('_')[1]}`; newTargetAllocations = {}; let ruleActionTotalAllocation = 0; rule.actions.forEach(action => { newTargetAllocations[action.assetId] = action.targetPercentage; ruleActionTotalAllocation += (action.targetPercentage || 0); }); // Ensure rule actions sum to 100%, if not, it's a definition error, could log or normalize if (Math.abs(ruleActionTotalAllocation - 100) > 0.1 && rule.actions.length > 0) { // If actions defined but not 100% console.warn(`Rule ${rule.id} actions do not sum to 100% (${ruleActionTotalAllocation}%). Normalizing.`); for(const assetId_norm in newTargetAllocations){ newTargetAllocations[assetId_norm] = (newTargetAllocations[assetId_norm] / ruleActionTotalAllocation) * 100; } } break; // First matching rule applies } } // 3. & 4. Apply rebalancing / new target allocations let rebalancePerformedThisStep = false; if (newTargetAllocations) { // Adaptive rule triggered currentAllocations = newTargetAllocations; // These are percentages rebalancePerformedThisStep = true; } else { // Check periodic/deviation rebalancing IF no adaptive rule triggered const rebalanceRule = aahfrs_model.rebalance; if (rebalanceRule.trigger === 'periodic' && ((stepIndex + 1) % (rebalanceRule.thresholdValue || 1)) === 0) { currentAllocations = { ...aahfrs_model.initialAllocationSet }; // Rebalance to initial targets rebalancePerformedThisStep = true; if(ruleTriggeredName === "None") ruleTriggeredName = "Periodic Rebalance"; } else if (rebalanceRule.trigger === 'deviation') { let deviationFound = false; for (const asset of aahfrs_model.assets) { const currentActualAlloc = currentPortfolioValue > 0 ? (assetValues[asset.id] / currentPortfolioValue * 100) : 0; const targetAlloc = currentAllocations[asset.id] || 0; // Use current target if no adaptive rule, or initial if that's the policy if (Math.abs(currentActualAlloc - targetAlloc) > (rebalanceRule.thresholdValue || 5)) { deviationFound = true; break; } } if (deviationFound) { // Rebalance to current targetAllocations (which might be initial if no rule has ever fired) // For simplicity, if a deviation occurs, assume rebalancing to the *last set* targetAllocations. // If no adaptive rule has fired yet, this implies rebalancing to initialAllocationSet. rebalancePerformedThisStep = true; if(ruleTriggeredName === "None") ruleTriggeredName = "Deviation Rebalance"; } } } if (rebalancePerformedThisStep) { let totalTransactionValue = 0; // Calculate how much $ value of each asset needs to change for rebalance aahfrs_model.assets.forEach(asset => { const currentAssetValue = assetValues[asset.id]; const newTargetAssetValue = currentPortfolioValue * ((currentAllocations[asset.id] || 0) / 100); totalTransactionValue += Math.abs(newTargetAssetValue - currentAssetValue); assetValues[asset.id] = newTargetAssetValue; // Update asset value based on new allocation }); const transactionCost = totalTransactionValue * (aahfrs_model.transactionFeePercent / 100); currentPortfolioValue -= transactionCost; // Deduct fees from total portfolio value stepPnl -= transactionCost; // Fees reduce P&L for the step // After deducting fees, ensure assetValues still reflect new allocations of the new currentPortfolioValue aahfrs_model.assets.forEach(asset => { assetValues[asset.id] = currentPortfolioValue * ((currentAllocations[asset.id] || 0) / 100); }); } // Store allocations after adaptation/rebalancing for logging aahfrs_model.assets.forEach(asset => { newAllocationsForLog[asset.name || asset.id] = currentPortfolioValue > 0 ? (assetValues[asset.id] / currentPortfolioValue * 100) : 0; }); portfolioValueHistory.push(currentPortfolioValue); // 5. Log step aahfrs_simulationLog.push({ step: stepIndex + 1, factorValues: JSON.stringify(stepData.factorValues).substring(0,100) + (JSON.stringify(stepData.factorValues).length > 100 ? '...':''), ruleTriggered: ruleTriggeredName, oldAllocations: Object.entries(oldAllocationsForLog).map(([k,v]) => `${k}:${v.toFixed(1)}%`).join(', '), newAllocations: Object.entries(newAllocationsForLog).map(([k,v]) => `${k}:${v.toFixed(1)}%`).join(', '), assetPerformance: Object.entries(assetPerfForLog).map(([k,v]) => `${k}:${v.toFixed(1)}%`).join(', '), stepPnl: stepPnl.toFixed(2), portfolioValue: currentPortfolioValue.toFixed(2) }); }); // Render Log Table aahfrs_simulationLog.forEach(logEntry => { const row = logBody.insertRow(); row.insertCell().textContent = logEntry.step; row.insertCell().textContent = logEntry.factorValues; row.insertCell().textContent = logEntry.ruleTriggered; row.insertCell().textContent = logEntry.oldAllocations; row.insertCell().textContent = logEntry.newAllocations; row.insertCell().textContent = logEntry.assetPerformance; const pnlCell = row.insertCell(); pnlCell.textContent = '$' + logEntry.stepPnl; pnlCell.className = parseFloat(logEntry.stepPnl) >= 0 ? 'aahfrs_profit' : 'aahfrs_loss'; row.insertCell().textContent = '$' + logEntry.portfolioValue; }); // Render Summary const totalReturn = currentPortfolioValue - aahfrs_model.initialCapital; const totalReturnPercent = (totalReturn / aahfrs_model.initialCapital) * 100; summaryDiv.innerHTML = `

Simulation Performance Summary

Initial Portfolio Value: $${aahfrs_model.initialCapital.toFixed(2)}

Final Portfolio Value: $${currentPortfolioValue.toFixed(2)}

Total Net P&L: $${totalReturn.toFixed(2)}

Total Return: ${totalReturnPercent.toFixed(2)}%

`; aahfrs_renderPortfolioChart(portfolioValueHistory); if (document.getElementById('aahfrs_downloadPdfButton')) document.getElementById('aahfrs_downloadPdfButton').style.display = 'block'; } function aahfrs_renderPortfolioChart(data) { const chartContainer = aahfrs_getEl('aahfrs_portfolioValueChart'); const containerEl = aahfrs_getEl('aahfrs_portfolioValueChartContainer'); if (!chartContainer || !containerEl) return; chartContainer.innerHTML = ''; containerEl.style.display = data.length > 1 ? 'block' : 'none'; const maxValue = Math.max(...data); const minValue = Math.min(...data); // Could be used for scaling from non-zero base data.forEach((value, index) => { const bar = document.createElement('div'); bar.className = 'aahfrs_chart_bar'; // Scale height relative to max value and container height const barHeight = maxValue > 0 ? (value / maxValue) * 100 : 0; bar.style.height = `${Math.max(0, barHeight)}%`; // Ensure non-negative height const valueLabel = document.createElement('div'); valueLabel.className = 'aahfrs_chart_bar_value'; valueLabel.textContent = Math.round(value).toLocaleString(); if (barHeight < 15) valueLabel.style.top = '-15px'; // Adjust label if bar is too short const stepLabel = document.createElement('div'); stepLabel.className = 'aahfrs_chart_bar_label'; stepLabel.textContent = `S${index}`; // S0 for initial bar.appendChild(valueLabel); bar.appendChild(stepLabel); chartContainer.appendChild(bar); }); } function aahfrs_collectModelInputs() { aahfrs_model.strategyName = aahfrs_getInputValue('aahfrs_strategyName', 'Unnamed Strategy'); aahfrs_model.archetype = aahfrs_getInputValue('aahfrs_archetype', 'general_adaptive'); aahfrs_model.initialCapital = aahfrs_getInputNumValue('aahfrs_initialCapital', 100000); aahfrs_model.transactionFeePercent = aahfrs_getInputNumValue('aahfrs_transactionFee', 0.05); aahfrs_model.rebalance.trigger = aahfrs_getInputValue('aahfrs_rebalanceTrigger', 'none'); aahfrs_model.rebalance.thresholdValue = aahfrs_getInputNumValue('aahfrs_rebalanceThresholdValue', 5); // Initial allocations are already in aahfrs_model.initialAllocationSet from their own update function // Factors, Assets, Rules, SimData are also updated directly into the model object } // --- PDF Generation --- function aahfrs_downloadPDF() { if (aahfrs_simulationLog.length === 0) { alert("Please run a simulation first to generate results for PDF."); return; } if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') { alert('PDF library (jsPDF) is not loaded.'); return; } const { jsPDF: JSPDF } = window.jspdf; const doc = new JSPDF(); let y = 15; const m = 15; const cw = doc.internal.pageSize.getWidth() - (2 * m); aahfrs_collectModelInputs(); // Ensure model is up-to-date function addLine(text, size, style = 'normal', indent = 0, spacing = 2.5) { if (y > 275) { doc.addPage(); y = m; } doc.setFontSize(size); doc.setFont(undefined, style); const lines = doc.splitTextToSize(text, cw - indent); doc.text(lines, m + indent, y); y += (lines.length * (size * 0.35)) + spacing; } addLine(`Adaptive Strategy: ${aahfrs_model.strategyName}`, 18, 'bold', 0, 5); addLine(`Archetype: ${aahfrs_model.archetype}, Initial Capital: $${aahfrs_model.initialCapital.toFixed(2)}`, 10); addLine(`Transaction Fee: ${aahfrs_model.transactionFeePercent}%`, 10, 'normal', 0, 6); addLine("Factors:", 14, 'bold'); aahfrs_model.factors.forEach(f => addLine(`- ${f.name}: ${f.type} ${f.type==='numerical' ? `(${f.scaleMin}-${f.scaleMax})` : `(${(f.categories||[]).join('/')})`}`, 9, 'normal', 5)); y+=3; addLine("Assets:", 14, 'bold'); aahfrs_model.assets.forEach(a => addLine(`- ${a.name}`, 9, 'normal', 5)); y+=3; addLine("Initial Allocation:", 14, 'bold'); for(const assetId in aahfrs_model.initialAllocationSet){ const asset = aahfrs_model.assets.find(a => a.id === assetId); if(asset) addLine(`- ${asset.name}: ${aahfrs_model.initialAllocationSet[assetId]}%`, 9, 'normal', 5); } y+=3; addLine(`Rebalancing: ${aahfrs_model.rebalance.trigger}, Threshold: ${aahfrs_model.rebalance.thresholdValue}${aahfrs_model.rebalance.trigger === 'deviation' ? '%' : (aahfrs_model.rebalance.trigger === 'periodic' ? ' steps' : '')}`, 10); y+=3; addLine("Adaptive Rules:", 14, 'bold'); aahfrs_model.rules.forEach((r, i) => { addLine(`Rule ${i+1}:`, 10, 'bold', 5); r.conditions.forEach(c => { const fDef = aahfrs_model.factors.find(f => f.id === c.factorId); addLine(` IF ${fDef ? fDef.name : c.factorId} ${c.operator} ${c.value}`, 9, 'normal', 10, 1); }); addLine(` THEN set allocations:`, 9, 'normal', 10, 1); r.actions.forEach(act => { const aDef = aahfrs_model.assets.find(a => a.id === act.assetId); addLine(` ${aDef ? aDef.name : act.assetId}: ${act.targetPercentage}%`, 9, 'normal', 15, 1); }); y+=1; }); y+=5; addLine("Simulation Results:", 16, 'bold', 0, 5); const summaryText = aahfrs_getEl('aahfrs_simulationSummary').innerText; addLine(summaryText.replace(/(\$NaN|NaN%)/g, '$0.00 (Error?)'), 10); y+=3; addLine("Simulation Log:", 14, 'bold'); aahfrs_simulationLog.forEach(log => { if (y > 270) { doc.addPage(); y = m; } let logLine = `Step ${log.step}: Factors (${log.factorValues.substring(0,30)}...), Rule: ${log.ruleTriggered}, P&L: $${log.stepPnl}, Value: $${log.portfolioValue}`; addLine(logLine, 7, 'normal', 0, 0.5); }); doc.save(`${aahfrs_model.strategyName.replace(/\s+/g, '_') || 'adaptive_strategy_sim'}.pdf`); }
Scroll to Top