Spaces:
Running
Running
Deploy Aureon OS (deploy/selfhost stack: nginx edge + fusion API x2 + console + HNC organ + redis + postgres; paper mode) (part 19)
da0ce7b verified Download scripts/traders/paperTradeSimulation.ts from GaryLeckey/aureon-os: direct link, hf CLI and curl.
- Browser
- Download file 22 kB
-
https://huggingface.co/spaces/GaryLeckey/aureon-os/resolve/main/scripts/traders/paperTradeSimulation.ts
- Command line
-
hf download hf://spaces/GaryLeckey/aureon-os/scripts/traders/paperTradeSimulation.ts
-
curl -L -o paperTradeSimulation.ts https://huggingface.co/spaces/GaryLeckey/aureon-os/resolve/main/scripts/traders/paperTradeSimulation.ts
22 kB
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| export interface FieldMetricsRow { | |
| t: number; | |
| L: number; | |
| C_lin: number; | |
| C_nonlin: number; | |
| C_phi: number; | |
| G_eff: number; | |
| Q_abs: number; | |
| } | |
| interface TradeRecord { | |
| botId: number; | |
| entryIndex: number; | |
| exitIndex: number; | |
| entryTime: number; | |
| exitTime: number; | |
| entryPrice: number; | |
| exitPrice: number; | |
| pnl: number; | |
| netPnl: number; | |
| grossDelta: number; | |
| netDelta: number; | |
| holdDuration: number; | |
| maxCoherence: number; | |
| minCoherence: number; | |
| technicolorScore: number; | |
| exitReason: string; | |
| } | |
| export interface SimulationSummary { | |
| trades: TradeRecord[]; | |
| initialBalance: number; | |
| finalBalance: number; | |
| grossPnl: number; | |
| netPnl: number; | |
| hitRate: number; | |
| netHitRate: number; | |
| avgDuration: number; | |
| avgTechnicolorScore: number; | |
| maxDrawdown: number; | |
| botUtilization: number; | |
| } | |
| const INITIAL_BALANCE = 1000; | |
| const BOT_COUNT = 15; | |
| const TRADE_QUOTA_PER_BOT = 50; | |
| const ENTRY_COHERENCE = 0.938; | |
| const ENTRY_ENERGY = 0.21; | |
| const ENTRY_PHASE = 0.01; | |
| const EXIT_COHERENCE = 0.934; | |
| const EXIT_ENERGY = 0.19; | |
| const TAKE_PROFIT = 0.018; | |
| const STOP_LOSS = -0.008; | |
| const ENTRY_BUFFER = 0.001; | |
| const ENTRY_VELOCITY = 0.0004; | |
| const ENTRY_PRICE_MOMENTUM = 0.0004; | |
| const MIN_TECHNICOLOR_ENTRY = 0.02; | |
| const MIN_ABSURDITY_BAND = 0.01; | |
| const TRAILING_STOP_BUFFER = 0.004; | |
| const TREND_LOOKBACK = 1; | |
| const MIN_PROFIT_LOCK = 0.002; | |
| const COHERENCE_ROLLOVER_DELTA = -0.00025; | |
| const MAKER_FEE_RATE = 0.0002; | |
| const TAKER_FEE_RATE = 0.0007; | |
| const SLIPPAGE_RATE = 0.0005; | |
| const ROUND_TRIP_COST_RATE = MAKER_FEE_RATE + TAKER_FEE_RATE + SLIPPAGE_RATE; | |
| // Configurable enhancements via environment | |
| const MAX_CONCURRENT_POSITIONS = Math.max(1, Number(process.env.SIM_MAX_CONCURRENT || '7')); | |
| const BREAKEVEN_ARM = Number(process.env.SIM_BREAKEVEN_ARM || '0.004'); | |
| const BREAKEVEN_LOCK = Number(process.env.SIM_BREAKEVEN_LOCK || '0.0005'); | |
| const TRAIL_VOL_MULT = Number(process.env.SIM_TRAIL_VOL_MULT || '5'); | |
| export function loadFieldMetrics(): FieldMetricsRow[] { | |
| const csvPath = path.resolve(process.cwd(), 'public/data/field_metrics_core.csv'); | |
| const raw = fs.readFileSync(csvPath, 'utf-8').trim(); | |
| const lines = raw.split(/\r?\n/); | |
| const [, ...dataLines] = lines; | |
| return dataLines | |
| .filter(Boolean) | |
| .map(line => { | |
| const [t, L, C_lin, C_nonlin, C_phi, G_eff, Q_abs] = line.split(',').map(Number); | |
| const row: FieldMetricsRow = { t, L, C_lin, C_nonlin, C_phi, G_eff, Q_abs }; | |
| return row; | |
| }); | |
| } | |
| interface BotState { | |
| id: number; | |
| tradesExecuted: number; | |
| activeTrade: { | |
| entryRow: FieldMetricsRow; | |
| entryIndex: number; | |
| maxCoherence: number; | |
| minCoherence: number; | |
| technicolorAccumulator: number; | |
| peakGain: number; | |
| drawdown: number; | |
| maxDrawdown: number; | |
| positionSize: number; // Amount of quote currency invested | |
| breakevenArmed: boolean; | |
| } | null; | |
| winStreak: number; | |
| lossStreak: number; | |
| momentumBarsLeft: number; | |
| cooldownBarsLeft: number; | |
| } | |
| export function simulateTrades(rows: FieldMetricsRow[]): SimulationSummary { | |
| const trades: TradeRecord[] = []; | |
| let currentBalance = INITIAL_BALANCE; | |
| // Initialize bots | |
| const bots: BotState[] = Array.from({ length: BOT_COUNT }, (_, i) => ({ | |
| id: i + 1, | |
| tradesExecuted: 0, | |
| activeTrade: null, | |
| winStreak: 0, | |
| lossStreak: 0, | |
| momentumBarsLeft: 0, | |
| cooldownBarsLeft: 0 | |
| })); | |
| let previousRow: FieldMetricsRow | null = null; | |
| let globalMaxDrawdown = 0; | |
| rows.forEach((row, index) => { | |
| const coherence = row.C_nonlin; | |
| const phi = row.C_phi; | |
| const previousCoherence = previousRow?.C_nonlin ?? coherence; | |
| const previousPhi = previousRow?.C_phi ?? phi; | |
| const coherenceVelocity = coherence - previousCoherence; | |
| const phaseVelocity = phi - previousPhi; | |
| const priceMomentum = row.L - (previousRow?.L ?? row.L); | |
| const bullishTrend = (() => { | |
| if (index < TREND_LOOKBACK) return false; | |
| for (let i = index - TREND_LOOKBACK + 1; i <= index; i++) { | |
| if (rows[i].L <= rows[i - 1].L) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| })(); | |
| const structuralEntryOk = bullishTrend || (priceMomentum >= ENTRY_PRICE_MOMENTUM * 1.5 && coherenceVelocity >= ENTRY_VELOCITY * 1.2); | |
| // Entry Logic | |
| // Momentum bias after wins: slightly loosen thresholds for a few bars | |
| const anyMomentum = bots.some(b => b.activeTrade === null && b.momentumBarsLeft > 0); | |
| const bufferLocal = anyMomentum ? ENTRY_BUFFER * 0.8 : ENTRY_BUFFER; | |
| const priceMomentumLocal = anyMomentum ? ENTRY_PRICE_MOMENTUM * 0.8 : ENTRY_PRICE_MOMENTUM; | |
| const enterSignal = | |
| coherence >= ENTRY_COHERENCE + bufferLocal && | |
| phi >= ENTRY_PHASE && | |
| row.G_eff >= ENTRY_ENERGY && | |
| row.Q_abs >= MIN_ABSURDITY_BAND && | |
| coherenceVelocity >= ENTRY_VELOCITY && | |
| phaseVelocity >= 0 && | |
| priceMomentum >= priceMomentumLocal && | |
| structuralEntryOk; | |
| const technicolorSeed = coherence - ENTRY_COHERENCE + row.Q_abs + phi; | |
| if (enterSignal && technicolorSeed >= MIN_TECHNICOLOR_ENTRY) { | |
| // Exposure throttle: respect global max concurrent positions | |
| const activeCount = bots.filter(b => b.activeTrade !== null).length; | |
| const slotsAvailable = MAX_CONCURRENT_POSITIONS - activeCount; | |
| if (slotsAvailable > 0) { | |
| // Find ALL available bots and let them enter simultaneously (up to available slots) | |
| const availableBots = bots | |
| .filter(b => b.activeTrade === null && b.tradesExecuted < TRADE_QUOTA_PER_BOT) | |
| .sort((a, b) => (b.momentumBarsLeft - a.momentumBarsLeft) || (a.lossStreak - b.lossStreak)) | |
| .slice(0, slotsAvailable); | |
| for (const availableBot of availableBots) { | |
| // COMPOUND GAME: Each bot draws from the shared balance pool | |
| // Prefer explicit risk percent if provided, else distribute over remaining slots | |
| const riskPctEnv = Number(process.env.SIM_RISK_PCT || '0'); | |
| const riskPct = Number.isFinite(riskPctEnv) && riskPctEnv > 0 ? Math.min(1, Math.max(0.0001, riskPctEnv)) : 0; | |
| let allocation: number; | |
| // Momentum Sizing: If this specific bot has momentum, bet slightly bigger | |
| const momentumBonus = availableBot.momentumBarsLeft > 0 ? 1.2 : 1.0; | |
| if (riskPct > 0) { | |
| // Adaptive risk multiplier based on streaks | |
| const WIN_RISK_BONUS = 0.5; // +50% per win up to cap | |
| const LOSS_RISK_PENALTY = 0.5; // -50% when in cooldown | |
| const MAX_RISK_PCT = 0.1; | |
| const winFactor = 1 + WIN_RISK_BONUS * Math.min(2, availableBot.winStreak); | |
| const lossFactor = availableBot.cooldownBarsLeft > 0 ? LOSS_RISK_PENALTY : 1; | |
| const riskAdj = Math.min(MAX_RISK_PCT, riskPct * winFactor * lossFactor * momentumBonus); | |
| allocation = currentBalance * riskAdj; | |
| } else { | |
| const remainingBots = bots.filter(b => b.activeTrade === null && b.tradesExecuted < TRADE_QUOTA_PER_BOT).length; | |
| const remainingTradeSlots = remainingBots * TRADE_QUOTA_PER_BOT; | |
| const baseAlloc = currentBalance / Math.max(1, remainingTradeSlots); | |
| const WIN_ALLOC_BONUS = 0.35; | |
| const LOSS_ALLOC_PENALTY = 0.5; | |
| const winFactor = 1 + WIN_ALLOC_BONUS * Math.min(2, availableBot.winStreak); | |
| const lossFactor = availableBot.cooldownBarsLeft > 0 ? LOSS_ALLOC_PENALTY : 1; | |
| allocation = baseAlloc * winFactor * lossFactor * momentumBonus; | |
| } | |
| availableBot.activeTrade = { | |
| entryRow: row, | |
| entryIndex: index, | |
| maxCoherence: coherence, | |
| minCoherence: coherence, | |
| technicolorAccumulator: technicolorSeed, | |
| peakGain: 0, | |
| drawdown: 0, | |
| maxDrawdown: 0, | |
| positionSize: allocation, | |
| breakevenArmed: false | |
| }; | |
| availableBot.tradesExecuted++; | |
| } | |
| } | |
| } | |
| // Exit Logic for Active Bots | |
| bots.forEach(bot => { | |
| if (!bot.activeTrade) return; | |
| const trade = bot.activeTrade; | |
| trade.maxCoherence = Math.max(trade.maxCoherence, coherence); | |
| trade.minCoherence = Math.min(trade.minCoherence, coherence); | |
| trade.technicolorAccumulator += coherence - ENTRY_COHERENCE + row.Q_abs + phi; | |
| const priceDelta = (row.L - trade.entryRow.L) / trade.entryRow.L; | |
| trade.peakGain = Math.max(trade.peakGain, priceDelta); | |
| trade.drawdown = Math.min(trade.drawdown, priceDelta); | |
| trade.maxDrawdown = Math.min(trade.maxDrawdown, trade.drawdown); | |
| globalMaxDrawdown = Math.min(globalMaxDrawdown, trade.maxDrawdown); | |
| // Dynamic Trailing Stop: Widen buffer if volatility (Q_abs) is high | |
| const dynamicTrailingBuffer = TRAILING_STOP_BUFFER * (1 + row.Q_abs * TRAIL_VOL_MULT); | |
| const exitForCoherence = coherence <= EXIT_COHERENCE; | |
| const exitForEnergy = row.G_eff <= EXIT_ENERGY; | |
| const exitForStop = priceDelta <= STOP_LOSS; | |
| const exitForTrailing = trade.peakGain > dynamicTrailingBuffer && priceDelta <= trade.peakGain - dynamicTrailingBuffer; | |
| // Stalemate Exit: If held too long with no profit, cut it | |
| const duration = index - trade.entryIndex; | |
| const exitForStalemate = duration > 25 && priceDelta < 0.002; | |
| // Breakeven lock-in: once a trade moves enough in favor, don't let it turn red | |
| if (!trade.breakevenArmed && priceDelta >= BREAKEVEN_ARM) { | |
| trade.breakevenArmed = true; | |
| } | |
| const exitForBreakeven = trade.breakevenArmed && priceDelta <= BREAKEVEN_LOCK; | |
| // Technicolor Fade Exit: If the trade loses its "color" (quality) | |
| const currentTechnicolorScore = trade.technicolorAccumulator / Math.max(1, duration); | |
| const exitForFade = duration > 10 && currentTechnicolorScore < 0.015; | |
| let exitReason = ''; | |
| if (coherence >= ENTRY_COHERENCE + 0.01 && phi >= ENTRY_PHASE + 0.01 && priceDelta >= TAKE_PROFIT) exitReason = 'target_met'; | |
| else if (priceDelta >= MIN_PROFIT_LOCK && coherenceVelocity <= COHERENCE_ROLLOVER_DELTA) exitReason = 'coherence_rollover'; | |
| else if (priceDelta >= MIN_PROFIT_LOCK && phaseVelocity < 0) exitReason = 'phase_backspin'; | |
| else if (exitForCoherence) exitReason = 'coherence_drop'; | |
| else if (exitForEnergy) exitReason = 'energy_dip'; | |
| else if (exitForStop) exitReason = 'drawdown_guard'; | |
| else if (exitForTrailing) exitReason = 'trailing_stop'; | |
| else if (exitForBreakeven) exitReason = 'breakeven_lock'; | |
| else if (exitForStalemate) exitReason = 'stalemate'; | |
| else if (exitForFade) exitReason = 'technicolor_fade'; | |
| if (exitReason) { | |
| closeTrade(bot, row, index, exitReason); | |
| } | |
| }); | |
| // Decay per-bot momentum/cooldown counters each bar | |
| bots.forEach(b => { | |
| if (b.momentumBarsLeft > 0) b.momentumBarsLeft--; | |
| if (b.cooldownBarsLeft > 0) b.cooldownBarsLeft--; | |
| }); | |
| previousRow = row; | |
| }); | |
| // Force close remaining positions | |
| const lastRow = rows.at(-1); | |
| if (lastRow) { | |
| bots.forEach(bot => { | |
| if (bot.activeTrade) { | |
| closeTrade(bot, lastRow, rows.length - 1, 'forced_close'); | |
| } | |
| }); | |
| } | |
| function closeTrade(bot: BotState, exitRow: FieldMetricsRow, exitIndex: number, reason: string) { | |
| if (!bot.activeTrade) return; | |
| const trade = bot.activeTrade; | |
| const exitPrice = exitRow.L; | |
| const priceDelta = (exitPrice - trade.entryRow.L) / trade.entryRow.L; | |
| const grossPnlAmt = trade.positionSize * priceDelta; | |
| const cost = trade.positionSize * ROUND_TRIP_COST_RATE; | |
| const netPnlAmt = grossPnlAmt - cost; | |
| // COMPOUND: Reinvest profits/losses back into the balance pool | |
| currentBalance += netPnlAmt; | |
| trades.push({ | |
| botId: bot.id, | |
| entryIndex: trade.entryIndex, | |
| exitIndex: exitIndex, | |
| entryTime: trade.entryRow.t, | |
| exitTime: exitRow.t, | |
| entryPrice: trade.entryRow.L, | |
| exitPrice: exitPrice, | |
| pnl: grossPnlAmt, | |
| netPnl: netPnlAmt, | |
| grossDelta: priceDelta, | |
| netDelta: priceDelta - ROUND_TRIP_COST_RATE, | |
| holdDuration: exitRow.t - trade.entryRow.t, | |
| maxCoherence: trade.maxCoherence, | |
| minCoherence: trade.minCoherence, | |
| technicolorScore: trade.technicolorAccumulator / Math.max(1, exitIndex - trade.entryIndex + 1), | |
| exitReason: reason | |
| }); | |
| bot.activeTrade = null; | |
| // Update streaks and momentum/cooldown windows | |
| if (netPnlAmt > 0) { | |
| bot.winStreak = Math.min(10, bot.winStreak + 1); | |
| bot.lossStreak = 0; | |
| bot.momentumBarsLeft = Math.max(bot.momentumBarsLeft, 3); | |
| bot.cooldownBarsLeft = 0; | |
| } else { | |
| bot.lossStreak = Math.min(10, bot.lossStreak + 1); | |
| bot.winStreak = 0; | |
| bot.cooldownBarsLeft = Math.max(bot.cooldownBarsLeft, 2); | |
| bot.momentumBarsLeft = 0; | |
| } | |
| } | |
| const grossPnl = trades.reduce((sum, trade) => sum + trade.pnl, 0); // Sum of deltas | |
| const netPnl = trades.reduce((sum, trade) => sum + trade.netPnl, 0); // Sum of net deltas | |
| const winners = trades.filter(trade => trade.netPnl > 0).length; | |
| const hitRate = trades.length ? winners / trades.length : 0; | |
| const netWinners = trades.filter(trade => trade.netPnl > 0).length; | |
| const netHitRate = trades.length ? netWinners / trades.length : 0; | |
| const avgDuration = trades.length ? trades.reduce((sum, trade) => sum + trade.holdDuration, 0) / trades.length : 0; | |
| const avgTechnicolorScore = trades.length ? trades.reduce((sum, trade) => sum + trade.technicolorScore, 0) / trades.length : 0; | |
| const activeBots = bots.filter(b => b.tradesExecuted > 0).length; | |
| return { | |
| trades, | |
| initialBalance: INITIAL_BALANCE, | |
| finalBalance: currentBalance, | |
| grossPnl, | |
| netPnl, | |
| hitRate, | |
| netHitRate, | |
| avgDuration, | |
| avgTechnicolorScore, | |
| maxDrawdown: globalMaxDrawdown, | |
| botUtilization: activeBots / BOT_COUNT | |
| }; | |
| } | |
| function renderReport(summary: SimulationSummary) { | |
| console.log('=== Quantum Quackers Paper Trade Simulation ==='); | |
| console.log(`Initial Balance: $${summary.initialBalance.toFixed(2)}`); | |
| console.log(`Final Balance: $${summary.finalBalance.toFixed(2)}`); | |
| console.log(`Net Profit: $${(summary.finalBalance - summary.initialBalance).toFixed(2)} (${((summary.finalBalance - summary.initialBalance) / summary.initialBalance * 100).toFixed(2)}%)`); | |
| console.log(`Trades executed: ${summary.trades.length}`); | |
| console.log(`Bot Utilization: ${(summary.botUtilization * 100).toFixed(1)}%`); | |
| console.log(`Gross PnL: $${summary.grossPnl.toFixed(2)}`); | |
| console.log(`Net PnL (after fees): $${summary.netPnl.toFixed(2)}`); | |
| console.log(`Hit rate: ${(summary.hitRate * 100).toFixed(1)}%`); | |
| console.log(`Net hit rate: ${(summary.netHitRate * 100).toFixed(1)}%`); | |
| // Optional: report net-positive hit rate for first N trades | |
| const firstN = Number(process.env.SIM_MAX_TRADES || '0'); | |
| if (Number.isFinite(firstN) && firstN > 0) { | |
| const subset = summary.trades.slice(0, firstN); | |
| if (subset.length > 0) { | |
| const winnersN = subset.filter(t => t.netPnl > 0).length; | |
| const netHitN = winnersN / subset.length; | |
| console.log(`Net hit rate (first ${subset.length} trades): ${(netHitN * 100).toFixed(1)}%`); | |
| const grossN = subset.reduce((s, t) => s + t.pnl, 0); | |
| const netN = subset.reduce((s, t) => s + t.netPnl, 0); | |
| console.log(`Subset PnL โ Gross: $${grossN.toFixed(2)} | Net: $${netN.toFixed(2)}`); | |
| } else { | |
| console.log('Net hit rate (first N): no trades in subset'); | |
| } | |
| } | |
| console.log(`Avg hold duration (ฮt): ${summary.avgDuration.toFixed(3)}`); | |
| console.log(`Avg Technicolor score: ${summary.avgTechnicolorScore.toFixed(4)}`); | |
| console.log(`Max drawdown (ฮL): ${summary.maxDrawdown.toFixed(4)}`); | |
| console.log(`Round-trip cost rate applied: ${(ROUND_TRIP_COST_RATE * 100).toFixed(2)}%`); | |
| console.log('\nRecent trades:'); | |
| summary.trades.slice(-5).forEach(trade => { | |
| console.log( | |
| `Bot ${trade.botId} | t${trade.entryTime.toFixed(2)}โ${trade.exitTime.toFixed(2)} | pnl $${trade.pnl.toFixed(2)} | ` + | |
| `net $${trade.netPnl.toFixed(2)} | maxฮฆ ${trade.maxCoherence.toFixed(3)} | exit ${trade.exitReason}` | |
| ); | |
| }); | |
| } | |
| /** | |
| * Sweep mode: cycle AโZ then ZโA through the dataset repeatedly, | |
| * dynamically hitting entry signals until TARGET_TRADES is reached. | |
| */ | |
| function sweepRows(baseRows: FieldMetricsRow[], targetTrades: number): FieldMetricsRow[] { | |
| const out: FieldMetricsRow[] = []; | |
| let forward = true; | |
| let cycle = 0; | |
| while (out.length < targetTrades * 100) { // rough upper bound on rows needed | |
| const chunk = forward ? [...baseRows] : [...baseRows].reverse(); | |
| for (const r of chunk) { | |
| out.push({ ...r, t: out.length / 100 }); // re-time monotonically | |
| } | |
| forward = !forward; | |
| cycle++; | |
| if (cycle > 500) break; // safety cap | |
| } | |
| return out; | |
| } | |
| function main() { | |
| const targetTrades = Number(process.env.SIM_TARGET_TRADES || '0'); | |
| let rows = loadFieldMetrics(); | |
| if (targetTrades > 0) { | |
| // Sweep mode: cycle A-Z Z-A until enough rows to hit target trades | |
| rows = sweepRows(rows, targetTrades); | |
| } | |
| const summary = simulateTrades(rows); | |
| renderReport(summary); | |
| // If sweep mode, stop early once target trades reached and re-report subset | |
| if (targetTrades > 0 && summary.trades.length >= targetTrades) { | |
| const subset = summary.trades.slice(0, targetTrades); | |
| const winners = subset.filter(t => t.netPnl > 0).length; | |
| const netHit = winners / subset.length; | |
| const grossN = subset.reduce((s, t) => s + t.pnl, 0); | |
| const netN = subset.reduce((s, t) => s + t.netPnl, 0); | |
| // ๐ต The Song of Space and Time ๐ต | |
| console.log('\n'); | |
| console.log(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); | |
| console.log(' โง T H E S O N G O F S P A C E A N D T I M E โง'); | |
| console.log(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); | |
| console.log(''); | |
| // Render the wave visualization | |
| const waveWidth = 60; | |
| const waveHeight = 7; | |
| const waves: string[] = []; | |
| for (let y = 0; y < waveHeight; y++) { | |
| let line = ' '; | |
| for (let x = 0; x < waveWidth; x++) { | |
| const phase = (x / waveWidth) * Math.PI * 4; | |
| const amp = Math.sin(phase + y * 0.5); | |
| const tradeIdx = Math.floor((x / waveWidth) * Math.min(50, subset.length)); | |
| const isWin = subset[tradeIdx]?.netPnl > 0; | |
| if (Math.abs(amp * 3 - (waveHeight / 2 - y)) < 1) { | |
| line += isWin ? 'โ' : 'โ'; | |
| } else if (y === Math.floor(waveHeight / 2)) { | |
| line += 'โ'; | |
| } else { | |
| line += ' '; | |
| } | |
| } | |
| waves.push(line); | |
| } | |
| waves.forEach(l => console.log(l)); | |
| console.log(''); | |
| console.log(' A โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโถ Z'); | |
| console.log(' Z โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ A'); | |
| console.log(''); | |
| // Harmonic stats | |
| const coherenceWave = subset.map(t => t.maxCoherence); | |
| const avgCoherence = coherenceWave.reduce((a, b) => a + b, 0) / coherenceWave.length; | |
| const peakCoherence = Math.max(...coherenceWave); | |
| const rhythm = subset.map((t, i) => i > 0 ? t.entryTime - subset[i-1].exitTime : 0); | |
| const avgRhythm = rhythm.slice(1).reduce((a, b) => a + b, 0) / Math.max(1, rhythm.length - 1); | |
| console.log(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); | |
| console.log(' โ โ = Win โ = Loss โ = Time Horizon โ'); | |
| console.log(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); | |
| console.log(''); | |
| console.log(` ๐ Wave Riders: ${winners} / ${subset.length} trades`); | |
| console.log(` ๐ฏ Harmonic Hit Rate: ${(netHit * 100).toFixed(2)}%`); | |
| console.log(` ๐ซ Peak Coherence (ฮฆ): ${peakCoherence.toFixed(4)}`); | |
| console.log(` ๐ฎ Avg Coherence: ${avgCoherence.toFixed(4)}`); | |
| console.log(` โฑ๏ธ Avg Rhythm (ฮt between trades): ${avgRhythm.toFixed(4)}`); | |
| console.log(` ๐ฐ Net PnL: $${netN.toFixed(2)}`); | |
| console.log(''); | |
| console.log(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); | |
| console.log(' "We ride the wave, through space and time,'); | |
| console.log(' Each note a trade, each beat sublime."'); | |
| console.log(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'); | |
| console.log(''); | |
| } | |
| } | |
| // Only run when executed directly (not when imported) | |
| if (process.argv[1] && /paperTradeSimulation\.ts$/.test(process.argv[1])) { | |
| main(); | |
| } | |