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.
Portfolio Value Over Time ($)
| Step | Factor Values | Rule Triggered | Old Alloc. (%) | New Alloc. (%) | Asset Perf. (%) | Step P&L ($) | Portfolio Value ($) |
|---|
Total for this rule: ${currentTotalAllocationForRule.toFixed(1)}% (should be 100%)
`; card.innerHTML = `Adaptive Rule ${rule.id.split('_')[1]}
${conditionsHTML} ${actionsHTML} `; container.appendChild(card); }); } // --- Tab 4: Initial Allocation & Rebalancing --- function aahfrs_renderInitialAllocations() { const container = aahfrs_getEl('aahfrs_initialAllocationContainer'); if (!container) return; container.innerHTML = ''; if (aahfrs_model.assets.length === 0) { container.innerHTML = 'Define assets in Tab 2 first.
'; aahfrs_updateTotalInitialAllocationDisplay(); return; } aahfrs_model.assets.forEach(asset => { const group = document.createElement('div'); group.className = 'aahfrs_input_group'; group.innerHTML = ` `; container.appendChild(group); }); aahfrs_updateTotalInitialAllocationDisplay(); } function aahfrs_updateInitialAllocation(assetId, value) { aahfrs_model.initialAllocationSet[assetId] = parseFloat(value) || 0; aahfrs_updateTotalInitialAllocationDisplay(); } function aahfrs_updateTotalInitialAllocationDisplay() { let total = 0; for (const assetId in aahfrs_model.initialAllocationSet) { total += aahfrs_model.initialAllocationSet[assetId]; } const displayEl = aahfrs_getEl('aahfrs_totalInitialAllocation'); if(displayEl) { displayEl.textContent = total.toFixed(1); displayEl.style.color = (Math.abs(total - 100) < 0.1) ? '#34D399' : '#F87171'; // Green if 100, Red otherwise } } function aahfrs_toggleRebalanceThreshold() { const trigger = aahfrs_getEl('aahfrs_rebalanceTrigger').value; const thresholdGroup = aahfrs_getEl('aahfrs_rebalanceThresholdGroup'); const thresholdLabel = aahfrs_getEl('aahfrs_rebalanceThresholdLabel'); if (trigger === 'periodic') { thresholdLabel.textContent = 'Rebalance Every X Steps:'; thresholdGroup.style.display = 'block'; } else if (trigger === 'deviation') { thresholdLabel.textContent = 'Deviation Threshold Y (%):'; thresholdGroup.style.display = 'block'; } else { thresholdGroup.style.display = 'none'; } aahfrs_model.rebalance.trigger = trigger; } // --- Tab 5: Simulation Data Input --- function aahfrs_addSimDataStep() { const id = `sim_step_${aahfrs_simStepIdCounter++}`; let factorValues = {}; aahfrs_model.factors.forEach(f => { if (f.type === 'categorical' && f.categories && f.categories.length > 0) factorValues[f.id] = f.categories[0]; else if (f.type === 'numerical') factorValues[f.id] = f.scaleMin || 0; else factorValues[f.id] = ''; }); let assetPerformance = {}; aahfrs_model.assets.forEach(a => assetPerformance[a.id] = 0); aahfrs_model.simulationData.push({ id: id, factorValues: factorValues, assetPerformance: assetPerformance }); aahfrs_renderSimDataStepsList(); } function aahfrs_removeSimDataStep(id) { aahfrs_model.simulationData = aahfrs_model.simulationData.filter(s => s.id !== id); aahfrs_renderSimDataStepsList(); } function aahfrs_updateSimFactorValue(stepId, factorId, value) { const step = aahfrs_model.simulationData.find(s => s.id === stepId); if (step) { const factor = aahfrs_model.factors.find(f => f.id === factorId); if(factor && factor.type === 'numerical') step.factorValues[factorId] = parseFloat(value); else step.factorValues[factorId] = value; } } function aahfrs_updateSimAssetPerf(stepId, assetId, value) { const step = aahfrs_model.simulationData.find(s => s.id === stepId); if (step) step.assetPerformance[assetId] = parseFloat(value) || 0; } function aahfrs_renderSimDataStepsList() { const container = aahfrs_getEl('aahfrs_simDataStepsList'); if (!container) return; container.innerHTML = ''; if (aahfrs_model.factors.length === 0 && aahfrs_model.assets.length === 0) { container.innerHTML = '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:
Asset Performance (%) for this Step:
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`); }