@tradecanvas/analytics
Advanced tools
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"Backtester.d.ts","sourceRoot":"","sources":["../src/Backtester.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAKhE,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EAOd,UAAU,EACX,MAAM,YAAY,CAAC;AAOpB;;;;;;;;;;;;GAYG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,QAAQ,CAAK;gBAET,IAAI,EAAE,eAAe;IAOjC,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,GAAG,cAAc;IA2C3D,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,gBAAgB;IAyBxB,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,UAAU;IAyBlB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,WAAW;CAMpB"} | ||
| {"version":3,"file":"Backtester.d.ts","sourceRoot":"","sources":["../src/Backtester.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAKhE,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EAOd,UAAU,EACX,MAAM,YAAY,CAAC;AAOpB;;;;;;;;;;;;GAYG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,QAAQ,CAAK;gBAET,IAAI,EAAE,eAAe;IAOjC,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,GAAG,cAAc;IA2C3D,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,gBAAgB;IAyBxB,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,UAAU;IAgClB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,WAAW;CAMpB"} |
+1
-1
@@ -1,2 +0,2 @@ | ||
| "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class C{constructor(t){this.perTrade=t}calculate(){return this.perTrade}}class D{constructor(t){this.rate=t}calculate(t,i){return Math.abs(t)*i*this.rate}}class v{constructor(t,i=0){this.perShare=t,this.minimum=i}calculate(t){return Math.max(this.minimum,Math.abs(t)*this.perShare)}}const P={calculate:()=>0},q={apply:s=>s};class z{constructor(t){this.rate=t}apply(t,i){const e=i==="long"?1+this.rate:1-this.rate;return t*e}}class T{constructor(t){this.factor=t}apply(t,i,e){const o=(e.high-e.low)*this.factor;return i==="long"?t+o:t-o}}class w{cash;initialCash;position=null;fills=[];trades=[];equityCurve=[];realizedPnl=0;constructor(t){if(t.initialCash<=0)throw new Error("initialCash must be > 0");this.cash=t.initialCash,this.initialCash=t.initialCash}getCash(){return this.cash}getPosition(){return this.position}getFills(){return this.fills}getTrades(){return this.trades}getEquityCurve(){return this.equityCurve}getInitialCash(){return this.initialCash}getRealizedPnl(){return this.realizedPnl}unrealizedPnl(t){if(!this.position)return 0;const i=this.position.side==="long"?1:-1;return(t-this.position.averagePrice)*this.position.quantity*i}equity(t){return this.cash+this.positionValue(t)}positionValue(t){if(!this.position)return 0;const i=this.position.side==="long"?1:-1;return this.position.quantity*t*i}applyFill(t){this.fills.push(t);const i=t.side==="long"?t.quantity:-t.quantity;if(this.cash-=i*t.price,this.cash-=t.commission,!this.position){this.position={side:t.side,quantity:t.quantity,averagePrice:t.price,openedAt:t.time,tag:t.tag};return}if(this.position.side===t.side){const r=this.position.quantity+t.quantity;this.position={...this.position,quantity:r,averagePrice:(this.position.averagePrice*this.position.quantity+t.price*t.quantity)/r};return}const e=Math.min(this.position.quantity,t.quantity),n=this.position.side==="long"?1:-1,o=(t.price-this.position.averagePrice)*e*n;this.realizedPnl+=o,this.trades.push({entryTime:this.position.openedAt,exitTime:t.time,side:this.position.side,quantity:e,entryPrice:this.position.averagePrice,exitPrice:t.price,pnl:o,pnlPct:(t.price/this.position.averagePrice-1)*n,commission:t.commission,tag:t.tag??this.position.tag});const a=this.position.quantity-t.quantity;a>0?this.position={...this.position,quantity:a}:a<0?this.position={side:t.side,quantity:-a,averagePrice:t.price,openedAt:t.time,tag:t.tag}:this.position=null}mark(t,i){const e=this.positionValue(i),n=this.unrealizedPnl(i);this.equityCurve.push({time:t,equity:this.cash+e,cash:this.cash,positionValue:e,unrealizedPnl:n,realizedPnl:this.realizedPnl})}reverseSide(t){return t==="long"?"short":"long"}}const S=365*24*60*60*1e3;function M(s,t,i,e={}){if(t.length<2)return A(s,t,i);const n=t[t.length-1].equity,o=n-s,a=o/s,r=e.periodsPerYear??N(t),l=(e.riskFreeRate??0)/r,c=_(t),u=O(c),d=B(c,u),g=L(c,l),x=d===0?0:(u-l)/d*Math.sqrt(r),F=g===0?0:(u-l)/g*Math.sqrt(r),m=t[t.length-1].time-t[0].time,f=m>0?m/S:0,y=f>0?Math.pow(n/s,1/f)-1:0,{maxDrawdown:k,maxDrawdownPct:p}=Y(t),E=p>0?y/p:0,I=R(i);return{totalReturn:o,totalReturnPct:a,cagr:y,sharpe:x,sortino:F,calmar:E,maxDrawdown:k,maxDrawdownPct:p,...I}}function A(s,t,i){const e=t.length>0?t[t.length-1].equity:s;return{totalReturn:e-s,totalReturnPct:(e-s)/s,cagr:0,sharpe:0,sortino:0,calmar:0,maxDrawdown:0,maxDrawdownPct:0,...R(i)}}function _(s){const t=[];for(let i=1;i<s.length;i++){const e=s[i-1].equity;if(e<=0){t.push(0);continue}t.push(s[i].equity/e-1)}return t}function O(s){if(s.length===0)return 0;let t=0;for(const i of s)t+=i;return t/s.length}function B(s,t){if(s.length<2)return 0;let i=0;for(const e of s)i+=(e-t)**2;return Math.sqrt(i/(s.length-1))}function L(s,t){if(s.length<2)return 0;let i=0,e=0;for(const n of s){const o=n-t;o<0&&(i+=o**2,e++)}return e===0?0:Math.sqrt(i/e)}function Y(s){let t=s[0].equity,i=0,e=0;for(const n of s){n.equity>t&&(t=n.equity);const o=t-n.equity;o>i&&(i=o,e=t>0?o/t:0)}return{maxDrawdown:i,maxDrawdownPct:e}}function R(s){if(s.length===0)return{winRate:0,profitFactor:0,expectancy:0,averageWin:0,averageLoss:0,trades:0};let t=0,i=0,e=0,n=0;for(const c of s)c.pnl>0?(t++,e+=c.pnl):c.pnl<0&&(i++,n+=-c.pnl);const o=t/s.length,a=t>0?e/t:0,r=i>0?n/i:0,h=n>0?e/n:e>0?1/0:0,l=o*a-(1-o)*r;return{winRate:o,profitFactor:h,expectancy:l,averageWin:a,averageLoss:r,trades:s.length}}function N(s){if(s.length<2)return 252;const t=[];for(let n=1;n<s.length&&n<50;n++)t.push(s[n].time-s[n-1].time);const i=O(t);if(i<=0)return 252;const e=S/i;return e>2e5?365*24*60:e>5e4?365*24*4:e>5e3?365*24:e>200?252:e>40?52:12}class V{commission;slippage;allowShort;portfolio;pendingOrders=[];orderSeq=0;constructor(t){this.commission=t.commission??P,this.slippage=t.slippage??q,this.allowShort=t.allowShort??!0,this.portfolio=new w({initialCash:t.initialCash})}run(t,i){if(t.length<2)throw new Error("Backtester requires at least 2 bars");for(let r=0;r<t.length;r++){const h=t[r];if(this.fillPendingOrders(h),this.portfolio.mark(h.time,h.close),r<t.length-1){const l=this.makeContext(h,r,t.slice(0,r+1));i(l)}}for(const r of this.pendingOrders)r.status==="pending"&&(r.status="cancelled");const e=this.portfolio.getEquityCurve(),n=this.portfolio.getInitialCash(),o=this.portfolio.getTrades(),a=e.length>0?e[e.length-1].equity:n;return{fills:this.portfolio.getFills(),trades:o,equityCurve:e,initialCash:n,finalEquity:a,metrics:M(n,e,o)}}fillPendingOrders(t){for(const i of this.pendingOrders){if(i.status!=="pending")continue;const e=this.resolveFillPrice(i,t);if(e===null){(i.timeInForce==="day"||i.timeInForce==="ioc")&&(i.status="cancelled");continue}const n=this.slippage.apply(e,i.side,t),o=this.commission.calculate(i.quantity,n),a={orderId:i.id,time:t.time,price:n,quantity:i.quantity,side:i.side,commission:o,slippage:Math.abs(n-e),tag:i.tag};this.portfolio.applyFill(a),i.status="filled"}this.pendingOrders=this.pendingOrders.filter(i=>i.status==="pending")}resolveFillPrice(t,i){switch(t.type){case"market":return i.open;case"limit":return t.price===void 0?null:t.side==="long"&&i.low<=t.price?Math.min(t.price,i.open):t.side==="short"&&i.high>=t.price?Math.max(t.price,i.open):null;case"stop":return t.price===void 0?null:t.side==="long"&&i.high>=t.price?Math.max(t.price,i.open):t.side==="short"&&i.low<=t.price?Math.min(t.price,i.open):null}}makeContext(t,i,e){const n=this.portfolio;return{bar:t,index:i,history:e,position:n.getPosition(),cash:n.getCash(),equity:n.equity(t.close),placeOrder:o=>this.placeOrder(o,t.time),close:o=>this.closePosition(t.time,o),cancel:o=>this.cancelOrder(o)}}placeOrder(t,i){if(t.quantity<=0)throw new Error("order quantity must be > 0");if(!this.allowShort&&t.side==="short")throw new Error("shorting is disabled");const e=t.id??`o-${++this.orderSeq}`;return this.pendingOrders.push({id:e,side:t.side,type:t.type,quantity:t.quantity,price:t.price,tag:t.tag,timeInForce:t.timeInForce??"gtc",status:"pending",placedAt:i}),e}closePosition(t,i){const e=this.portfolio.getPosition();if(!e)return null;const n=e.side==="long"?"short":"long";return this.placeOrder({side:n,type:"market",quantity:e.quantity,tag:i},t)}cancelOrder(t){const i=this.pendingOrders.find(e=>e.id===t);return!i||i.status!=="pending"?!1:(i.status="cancelled",!0)}}exports.Backtester=V;exports.FixedCommission=C;exports.NO_SLIPPAGE=q;exports.PerShareCommission=v;exports.PercentCommission=D;exports.PercentSlippage=z;exports.Portfolio=w;exports.RangeBasedSlippage=T;exports.ZERO_COMMISSION=P;exports.computeRiskMetrics=M; | ||
| "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class C{constructor(t){this.perTrade=t}calculate(){return this.perTrade}}class D{constructor(t){this.rate=t}calculate(t,i){return Math.abs(t)*i*this.rate}}class v{constructor(t,i=0){this.perShare=t,this.minimum=i}calculate(t){return Math.max(this.minimum,Math.abs(t)*this.perShare)}}const P={calculate:()=>0},q={apply:s=>s};class z{constructor(t){this.rate=t}apply(t,i){const e=i==="long"?1+this.rate:1-this.rate;return t*e}}class T{constructor(t){this.factor=t}apply(t,i,e){const o=(e.high-e.low)*this.factor;return i==="long"?t+o:t-o}}class w{cash;initialCash;position=null;fills=[];trades=[];equityCurve=[];realizedPnl=0;constructor(t){if(t.initialCash<=0)throw new Error("initialCash must be > 0");this.cash=t.initialCash,this.initialCash=t.initialCash}getCash(){return this.cash}getPosition(){return this.position}getFills(){return this.fills}getTrades(){return this.trades}getEquityCurve(){return this.equityCurve}getInitialCash(){return this.initialCash}getRealizedPnl(){return this.realizedPnl}unrealizedPnl(t){if(!this.position)return 0;const i=this.position.side==="long"?1:-1;return(t-this.position.averagePrice)*this.position.quantity*i}equity(t){return this.cash+this.positionValue(t)}positionValue(t){if(!this.position)return 0;const i=this.position.side==="long"?1:-1;return this.position.quantity*t*i}applyFill(t){this.fills.push(t);const i=t.side==="long"?t.quantity:-t.quantity;if(this.cash-=i*t.price,this.cash-=t.commission,!this.position){this.position={side:t.side,quantity:t.quantity,averagePrice:t.price,openedAt:t.time,tag:t.tag};return}if(this.position.side===t.side){const r=this.position.quantity+t.quantity;this.position={...this.position,quantity:r,averagePrice:(this.position.averagePrice*this.position.quantity+t.price*t.quantity)/r};return}const e=Math.min(this.position.quantity,t.quantity),n=this.position.side==="long"?1:-1,o=(t.price-this.position.averagePrice)*e*n;this.realizedPnl+=o,this.trades.push({entryTime:this.position.openedAt,exitTime:t.time,side:this.position.side,quantity:e,entryPrice:this.position.averagePrice,exitPrice:t.price,pnl:o,pnlPct:(t.price/this.position.averagePrice-1)*n,commission:t.commission,tag:t.tag??this.position.tag});const a=this.position.quantity-t.quantity;a>0?this.position={...this.position,quantity:a}:a<0?this.position={side:t.side,quantity:-a,averagePrice:t.price,openedAt:t.time,tag:t.tag}:this.position=null}mark(t,i){const e=this.positionValue(i),n=this.unrealizedPnl(i);this.equityCurve.push({time:t,equity:this.cash+e,cash:this.cash,positionValue:e,unrealizedPnl:n,realizedPnl:this.realizedPnl})}reverseSide(t){return t==="long"?"short":"long"}}const S=365*24*60*60*1e3;function M(s,t,i,e={}){if(t.length<2)return L(s,t,i);const n=t[t.length-1].equity,o=n-s,a=o/s,r=e.periodsPerYear??N(t),h=(e.riskFreeRate??0)/r,c=A(t),u=O(c),d=_(c,u),g=B(c,h),R=d===0?0:(u-h)/d*Math.sqrt(r),F=g===0?0:(u-h)/g*Math.sqrt(r),m=t[t.length-1].time-t[0].time,f=m>0?m/S:0,y=f>0?Math.pow(n/s,1/f)-1:0,{maxDrawdown:E,maxDrawdownPct:p}=Y(t),k=p>0?y/p:0,I=x(i);return{totalReturn:o,totalReturnPct:a,cagr:y,sharpe:R,sortino:F,calmar:k,maxDrawdown:E,maxDrawdownPct:p,...I}}function L(s,t,i){const e=t.length>0?t[t.length-1].equity:s;return{totalReturn:e-s,totalReturnPct:(e-s)/s,cagr:0,sharpe:0,sortino:0,calmar:0,maxDrawdown:0,maxDrawdownPct:0,...x(i)}}function A(s){const t=[];for(let i=1;i<s.length;i++){const e=s[i-1].equity;if(e<=0){t.push(0);continue}t.push(s[i].equity/e-1)}return t}function O(s){if(s.length===0)return 0;let t=0;for(const i of s)t+=i;return t/s.length}function _(s,t){if(s.length<2)return 0;let i=0;for(const e of s)i+=(e-t)**2;return Math.sqrt(i/(s.length-1))}function B(s,t){if(s.length<2)return 0;let i=0,e=0;for(const n of s){const o=n-t;o<0&&(i+=o**2,e++)}return e===0?0:Math.sqrt(i/e)}function Y(s){let t=s[0].equity,i=0,e=0;for(const n of s){n.equity>t&&(t=n.equity);const o=t-n.equity;o>i&&(i=o,e=t>0?o/t:0)}return{maxDrawdown:i,maxDrawdownPct:e}}function x(s){if(s.length===0)return{winRate:0,profitFactor:0,expectancy:0,averageWin:0,averageLoss:0,trades:0};let t=0,i=0,e=0,n=0;for(const c of s)c.pnl>0?(t++,e+=c.pnl):c.pnl<0&&(i++,n+=-c.pnl);const o=t/s.length,a=t>0?e/t:0,r=i>0?n/i:0,l=n>0?e/n:e>0?1/0:0,h=o*a-(1-o)*r;return{winRate:o,profitFactor:l,expectancy:h,averageWin:a,averageLoss:r,trades:s.length}}function N(s){if(s.length<2)return 252;const t=[];for(let n=1;n<s.length&&n<50;n++)t.push(s[n].time-s[n-1].time);const i=O(t);if(i<=0)return 252;const e=S/i;return e>2e5?365*24*60:e>5e4?365*24*4:e>5e3?365*24:e>200?252:e>40?52:12}class V{commission;slippage;allowShort;portfolio;pendingOrders=[];orderSeq=0;constructor(t){this.commission=t.commission??P,this.slippage=t.slippage??q,this.allowShort=t.allowShort??!0,this.portfolio=new w({initialCash:t.initialCash})}run(t,i){if(t.length<2)throw new Error("Backtester requires at least 2 bars");for(let r=0;r<t.length;r++){const l=t[r];if(this.fillPendingOrders(l),this.portfolio.mark(l.time,l.close),r<t.length-1){const h=this.makeContext(l,r,t.slice(0,r+1));i(h)}}for(const r of this.pendingOrders)r.status==="pending"&&(r.status="cancelled");const e=this.portfolio.getEquityCurve(),n=this.portfolio.getInitialCash(),o=this.portfolio.getTrades(),a=e.length>0?e[e.length-1].equity:n;return{fills:this.portfolio.getFills(),trades:o,equityCurve:e,initialCash:n,finalEquity:a,metrics:M(n,e,o)}}fillPendingOrders(t){for(const i of this.pendingOrders){if(i.status!=="pending")continue;const e=this.resolveFillPrice(i,t);if(e===null){(i.timeInForce==="day"||i.timeInForce==="ioc")&&(i.status="cancelled");continue}const n=this.slippage.apply(e,i.side,t),o=this.commission.calculate(i.quantity,n),a={orderId:i.id,time:t.time,price:n,quantity:i.quantity,side:i.side,commission:o,slippage:Math.abs(n-e),tag:i.tag};this.portfolio.applyFill(a),i.status="filled"}this.pendingOrders=this.pendingOrders.filter(i=>i.status==="pending")}resolveFillPrice(t,i){switch(t.type){case"market":return i.open;case"limit":return t.price===void 0?null:t.side==="long"&&i.low<=t.price?Math.min(t.price,i.open):t.side==="short"&&i.high>=t.price?Math.max(t.price,i.open):null;case"stop":return t.price===void 0?null:t.side==="long"&&i.high>=t.price?Math.max(t.price,i.open):t.side==="short"&&i.low<=t.price?Math.min(t.price,i.open):null}}makeContext(t,i,e){const n=this.portfolio;return{bar:t,index:i,history:e,position:n.getPosition(),cash:n.getCash(),equity:n.equity(t.close),placeOrder:o=>this.placeOrder(o,t.time),close:o=>this.closePosition(t.time,o),cancel:o=>this.cancelOrder(o)}}placeOrder(t,i){if(t.quantity<=0)throw new Error("order quantity must be > 0");if(!this.allowShort&&t.side==="short"){const n=this.portfolio.getPosition();if(!(n!==null&&n.side==="long"&&t.quantity<=n.quantity))throw new Error("shorting is disabled")}const e=t.id??`o-${++this.orderSeq}`;return this.pendingOrders.push({id:e,side:t.side,type:t.type,quantity:t.quantity,price:t.price,tag:t.tag,timeInForce:t.timeInForce??"gtc",status:"pending",placedAt:i}),e}closePosition(t,i){const e=this.portfolio.getPosition();if(!e)return null;const n=e.side==="long"?"short":"long";return this.placeOrder({side:n,type:"market",quantity:e.quantity,tag:i},t)}cancelOrder(t){const i=this.pendingOrders.find(e=>e.id===t);return!i||i.status!=="pending"?!1:(i.status="cancelled",!0)}}exports.Backtester=V;exports.FixedCommission=C;exports.NO_SLIPPAGE=q;exports.PerShareCommission=v;exports.PercentCommission=D;exports.PercentSlippage=z;exports.Portfolio=w;exports.RangeBasedSlippage=T;exports.ZERO_COMMISSION=P;exports.computeRiskMetrics=M; | ||
| //# sourceMappingURL=index.cjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.cjs","sources":["../src/commission.ts","../src/slippage.ts","../src/Portfolio.ts","../src/RiskMetrics.ts","../src/Backtester.ts"],"sourcesContent":["import type { CommissionModel } from './types.js';\n\nexport class FixedCommission implements CommissionModel {\n constructor(private readonly perTrade: number) {}\n\n calculate(): number {\n return this.perTrade;\n }\n}\n\nexport class PercentCommission implements CommissionModel {\n /** rate = 0.001 → 10 bps per trade notional. */\n constructor(private readonly rate: number) {}\n\n calculate(quantity: number, price: number): number {\n return Math.abs(quantity) * price * this.rate;\n }\n}\n\nexport class PerShareCommission implements CommissionModel {\n /** Minimum total commission per trade (optional). */\n constructor(\n private readonly perShare: number,\n private readonly minimum = 0,\n ) {}\n\n calculate(quantity: number): number {\n return Math.max(this.minimum, Math.abs(quantity) * this.perShare);\n }\n}\n\nexport const ZERO_COMMISSION: CommissionModel = {\n calculate: () => 0,\n};\n","import type { OHLCBar } from '@tradecanvas/commons';\nimport type { Side, SlippageModel } from './types.js';\n\nexport const NO_SLIPPAGE: SlippageModel = {\n apply: (price) => price,\n};\n\nexport class PercentSlippage implements SlippageModel {\n /** rate = 0.0005 → 5bps adverse */\n constructor(private readonly rate: number) {}\n\n apply(intendedPrice: number, side: Side): number {\n const adverse = side === 'long' ? 1 + this.rate : 1 - this.rate;\n return intendedPrice * adverse;\n }\n}\n\nexport class RangeBasedSlippage implements SlippageModel {\n /** factor = 0.1 → 10% of the bar's range pushes against the order */\n constructor(private readonly factor: number) {}\n\n apply(intendedPrice: number, side: Side, bar: OHLCBar): number {\n const range = bar.high - bar.low;\n const push = range * this.factor;\n return side === 'long' ? intendedPrice + push : intendedPrice - push;\n }\n}\n","import type {\n ClosedTrade,\n EquityPoint,\n Fill,\n PortfolioPosition,\n Side,\n} from './types.js';\n\nexport interface PortfolioOptions {\n initialCash: number;\n}\n\n/**\n * Tracks cash, a single net position, realized PnL, and the equity curve.\n *\n * Simplifying assumptions:\n * - One symbol at a time. Opposing fills net against the existing position.\n * - Realized PnL is computed when a fill reduces or flips the position.\n * - Equity = cash + position market value (mark-to-market).\n */\nexport class Portfolio {\n private cash: number;\n private readonly initialCash: number;\n private position: PortfolioPosition | null = null;\n private readonly fills: Fill[] = [];\n private readonly trades: ClosedTrade[] = [];\n private readonly equityCurve: EquityPoint[] = [];\n private realizedPnl = 0;\n\n constructor(opts: PortfolioOptions) {\n if (opts.initialCash <= 0) throw new Error('initialCash must be > 0');\n this.cash = opts.initialCash;\n this.initialCash = opts.initialCash;\n }\n\n getCash(): number {\n return this.cash;\n }\n\n getPosition(): Readonly<PortfolioPosition> | null {\n return this.position;\n }\n\n getFills(): ReadonlyArray<Fill> {\n return this.fills;\n }\n\n getTrades(): ReadonlyArray<ClosedTrade> {\n return this.trades;\n }\n\n getEquityCurve(): ReadonlyArray<EquityPoint> {\n return this.equityCurve;\n }\n\n getInitialCash(): number {\n return this.initialCash;\n }\n\n getRealizedPnl(): number {\n return this.realizedPnl;\n }\n\n unrealizedPnl(price: number): number {\n if (!this.position) return 0;\n const dir = this.position.side === 'long' ? 1 : -1;\n return (price - this.position.averagePrice) * this.position.quantity * dir;\n }\n\n equity(price: number): number {\n return this.cash + this.positionValue(price);\n }\n\n positionValue(price: number): number {\n if (!this.position) return 0;\n const dir = this.position.side === 'long' ? 1 : -1;\n return this.position.quantity * price * dir;\n }\n\n /** Apply a fill: cash flow + position update + realized PnL. */\n applyFill(fill: Fill): void {\n this.fills.push(fill);\n\n const signed = fill.side === 'long' ? fill.quantity : -fill.quantity;\n this.cash -= signed * fill.price;\n this.cash -= fill.commission;\n\n if (!this.position) {\n this.position = {\n side: fill.side,\n quantity: fill.quantity,\n averagePrice: fill.price,\n openedAt: fill.time,\n tag: fill.tag,\n };\n return;\n }\n\n if (this.position.side === fill.side) {\n // Same direction: average up\n const totalQty = this.position.quantity + fill.quantity;\n this.position = {\n ...this.position,\n quantity: totalQty,\n averagePrice:\n (this.position.averagePrice * this.position.quantity +\n fill.price * fill.quantity) /\n totalQty,\n };\n return;\n }\n\n // Opposite direction: close or flip\n const closing = Math.min(this.position.quantity, fill.quantity);\n const dir = this.position.side === 'long' ? 1 : -1;\n const pnl = (fill.price - this.position.averagePrice) * closing * dir;\n\n this.realizedPnl += pnl;\n this.trades.push({\n entryTime: this.position.openedAt,\n exitTime: fill.time,\n side: this.position.side,\n quantity: closing,\n entryPrice: this.position.averagePrice,\n exitPrice: fill.price,\n pnl,\n pnlPct: (fill.price / this.position.averagePrice - 1) * dir,\n commission: fill.commission,\n tag: fill.tag ?? this.position.tag,\n });\n\n const remaining = this.position.quantity - fill.quantity;\n if (remaining > 0) {\n this.position = { ...this.position, quantity: remaining };\n } else if (remaining < 0) {\n this.position = {\n side: fill.side,\n quantity: -remaining,\n averagePrice: fill.price,\n openedAt: fill.time,\n tag: fill.tag,\n };\n } else {\n this.position = null;\n }\n }\n\n /** Snapshot equity at the current bar close. */\n mark(time: number, price: number): void {\n const positionValue = this.positionValue(price);\n const unrealizedPnl = this.unrealizedPnl(price);\n this.equityCurve.push({\n time,\n equity: this.cash + positionValue,\n cash: this.cash,\n positionValue,\n unrealizedPnl,\n realizedPnl: this.realizedPnl,\n });\n }\n\n reverseSide(side: Side): Side {\n return side === 'long' ? 'short' : 'long';\n }\n}\n","import type {\n ClosedTrade,\n EquityPoint,\n RiskMetrics,\n} from './types.js';\n\nconst MS_PER_YEAR = 365 * 24 * 60 * 60 * 1000;\n\nexport interface RiskMetricsOptions {\n /** Periods per year for Sharpe/Sortino annualization. Auto-detected from equity timestamps if omitted. */\n periodsPerYear?: number;\n /** Annual risk-free rate, default 0. */\n riskFreeRate?: number;\n}\n\n/**\n * Compute summary risk and return metrics from an equity curve and closed trades.\n *\n * Returns NaN/0 fallbacks for degenerate inputs (empty curve, single point, no losses)\n * so downstream UIs can render safely.\n */\nexport function computeRiskMetrics(\n initialCash: number,\n equityCurve: ReadonlyArray<EquityPoint>,\n trades: ReadonlyArray<ClosedTrade>,\n opts: RiskMetricsOptions = {},\n): RiskMetrics {\n if (equityCurve.length < 2) {\n return emptyMetrics(initialCash, equityCurve, trades);\n }\n\n const finalEquity = equityCurve[equityCurve.length - 1].equity;\n const totalReturn = finalEquity - initialCash;\n const totalReturnPct = totalReturn / initialCash;\n\n const periodsPerYear =\n opts.periodsPerYear ?? inferPeriodsPerYear(equityCurve);\n const riskFreeRate = opts.riskFreeRate ?? 0;\n const periodRiskFree = riskFreeRate / periodsPerYear;\n\n const returns = periodReturns(equityCurve);\n const meanReturn = mean(returns);\n const stdDev = standardDeviation(returns, meanReturn);\n const downsideDev = downsideDeviation(returns, periodRiskFree);\n\n const sharpe = stdDev === 0\n ? 0\n : ((meanReturn - periodRiskFree) / stdDev) * Math.sqrt(periodsPerYear);\n const sortino = downsideDev === 0\n ? 0\n : ((meanReturn - periodRiskFree) / downsideDev) * Math.sqrt(periodsPerYear);\n\n const elapsedMs =\n equityCurve[equityCurve.length - 1].time - equityCurve[0].time;\n const years = elapsedMs > 0 ? elapsedMs / MS_PER_YEAR : 0;\n const cagr = years > 0\n ? Math.pow(finalEquity / initialCash, 1 / years) - 1\n : 0;\n\n const { maxDrawdown, maxDrawdownPct } = computeDrawdown(equityCurve);\n const calmar = maxDrawdownPct > 0 ? cagr / maxDrawdownPct : 0;\n\n const tradeStats = summarizeTrades(trades);\n\n return {\n totalReturn,\n totalReturnPct,\n cagr,\n sharpe,\n sortino,\n calmar,\n maxDrawdown,\n maxDrawdownPct,\n ...tradeStats,\n };\n}\n\nfunction emptyMetrics(\n initialCash: number,\n equityCurve: ReadonlyArray<EquityPoint>,\n trades: ReadonlyArray<ClosedTrade>,\n): RiskMetrics {\n const finalEquity = equityCurve.length > 0\n ? equityCurve[equityCurve.length - 1].equity\n : initialCash;\n return {\n totalReturn: finalEquity - initialCash,\n totalReturnPct: (finalEquity - initialCash) / initialCash,\n cagr: 0,\n sharpe: 0,\n sortino: 0,\n calmar: 0,\n maxDrawdown: 0,\n maxDrawdownPct: 0,\n ...summarizeTrades(trades),\n };\n}\n\nfunction periodReturns(curve: ReadonlyArray<EquityPoint>): number[] {\n const result: number[] = [];\n for (let i = 1; i < curve.length; i++) {\n const prev = curve[i - 1].equity;\n if (prev <= 0) {\n result.push(0);\n continue;\n }\n result.push(curve[i].equity / prev - 1);\n }\n return result;\n}\n\nfunction mean(values: number[]): number {\n if (values.length === 0) return 0;\n let sum = 0;\n for (const v of values) sum += v;\n return sum / values.length;\n}\n\nfunction standardDeviation(values: number[], avg: number): number {\n if (values.length < 2) return 0;\n let acc = 0;\n for (const v of values) acc += (v - avg) ** 2;\n return Math.sqrt(acc / (values.length - 1));\n}\n\nfunction downsideDeviation(values: number[], target: number): number {\n if (values.length < 2) return 0;\n let acc = 0;\n let count = 0;\n for (const v of values) {\n const diff = v - target;\n if (diff < 0) {\n acc += diff ** 2;\n count++;\n }\n }\n if (count === 0) return 0;\n return Math.sqrt(acc / count);\n}\n\nfunction computeDrawdown(curve: ReadonlyArray<EquityPoint>): {\n maxDrawdown: number;\n maxDrawdownPct: number;\n} {\n let peak = curve[0].equity;\n let maxDd = 0;\n let maxDdPct = 0;\n for (const point of curve) {\n if (point.equity > peak) peak = point.equity;\n const dd = peak - point.equity;\n if (dd > maxDd) {\n maxDd = dd;\n maxDdPct = peak > 0 ? dd / peak : 0;\n }\n }\n return { maxDrawdown: maxDd, maxDrawdownPct: maxDdPct };\n}\n\nfunction summarizeTrades(trades: ReadonlyArray<ClosedTrade>): {\n winRate: number;\n profitFactor: number;\n expectancy: number;\n averageWin: number;\n averageLoss: number;\n trades: number;\n} {\n if (trades.length === 0) {\n return {\n winRate: 0,\n profitFactor: 0,\n expectancy: 0,\n averageWin: 0,\n averageLoss: 0,\n trades: 0,\n };\n }\n\n let wins = 0;\n let losses = 0;\n let totalWin = 0;\n let totalLoss = 0;\n for (const t of trades) {\n if (t.pnl > 0) {\n wins++;\n totalWin += t.pnl;\n } else if (t.pnl < 0) {\n losses++;\n totalLoss += -t.pnl;\n }\n }\n\n const winRate = wins / trades.length;\n const averageWin = wins > 0 ? totalWin / wins : 0;\n const averageLoss = losses > 0 ? totalLoss / losses : 0;\n const profitFactor = totalLoss > 0 ? totalWin / totalLoss : totalWin > 0 ? Infinity : 0;\n const expectancy = winRate * averageWin - (1 - winRate) * averageLoss;\n\n return {\n winRate,\n profitFactor,\n expectancy,\n averageWin,\n averageLoss,\n trades: trades.length,\n };\n}\n\nfunction inferPeriodsPerYear(curve: ReadonlyArray<EquityPoint>): number {\n if (curve.length < 2) return 252;\n const deltas: number[] = [];\n for (let i = 1; i < curve.length && i < 50; i++) {\n deltas.push(curve[i].time - curve[i - 1].time);\n }\n const avgMs = mean(deltas);\n if (avgMs <= 0) return 252;\n const perYear = MS_PER_YEAR / avgMs;\n // Snap to common cadences for stability\n if (perYear > 200_000) return 365 * 24 * 60; // 1m\n if (perYear > 50_000) return 365 * 24 * 4; // 15m\n if (perYear > 5_000) return 365 * 24; // hourly\n if (perYear > 200) return 252; // daily trading\n if (perYear > 40) return 52; // weekly\n return 12; // monthly fallback\n}\n","import type { OHLCBar, DataSeries } from '@tradecanvas/commons';\nimport { ZERO_COMMISSION } from './commission.js';\nimport { NO_SLIPPAGE } from './slippage.js';\nimport { Portfolio } from './Portfolio.js';\nimport { computeRiskMetrics } from './RiskMetrics.js';\nimport type {\n BacktestOptions,\n BacktestOrder,\n BacktestResult,\n CommissionModel,\n Fill,\n OrderStatus,\n Side,\n SlippageModel,\n StrategyContext,\n StrategyFn,\n} from './types.js';\n\ninterface PendingOrder extends BacktestOrder {\n status: OrderStatus;\n placedAt: number;\n}\n\n/**\n * Bar-by-bar backtest engine.\n *\n * Execution model:\n * - Strategy fn runs at close of each bar\n * - Orders placed on bar N fill on bar N+1's open (market) or when\n * bar N+1 trades through the limit/stop price\n * - Equity is marked to close of every bar\n *\n * The engine is intentionally headless — no chart dependency — and produces\n * a result that can be visualised via @tradecanvas/chart's EquityCurveRenderer\n * or rendered into a report. See README for wiring examples.\n */\nexport class Backtester {\n private readonly commission: CommissionModel;\n private readonly slippage: SlippageModel;\n private readonly allowShort: boolean;\n private readonly portfolio: Portfolio;\n private pendingOrders: PendingOrder[] = [];\n private orderSeq = 0;\n\n constructor(opts: BacktestOptions) {\n this.commission = opts.commission ?? ZERO_COMMISSION;\n this.slippage = opts.slippage ?? NO_SLIPPAGE;\n this.allowShort = opts.allowShort ?? true;\n this.portfolio = new Portfolio({ initialCash: opts.initialCash });\n }\n\n run(data: DataSeries, strategy: StrategyFn): BacktestResult {\n if (data.length < 2) {\n throw new Error('Backtester requires at least 2 bars');\n }\n\n for (let i = 0; i < data.length; i++) {\n const bar = data[i];\n\n // 1. Fill any pending orders against this bar\n this.fillPendingOrders(bar);\n\n // 2. Mark-to-market after fills resolve\n this.portfolio.mark(bar.time, bar.close);\n\n // 3. Run strategy for the next bar (if there is one)\n if (i < data.length - 1) {\n const ctx = this.makeContext(bar, i, data.slice(0, i + 1));\n strategy(ctx);\n }\n }\n\n // Cancel anything left pending\n for (const order of this.pendingOrders) {\n if (order.status === 'pending') order.status = 'cancelled';\n }\n\n const equityCurve = this.portfolio.getEquityCurve();\n const initialCash = this.portfolio.getInitialCash();\n const trades = this.portfolio.getTrades();\n const finalEquity = equityCurve.length > 0\n ? equityCurve[equityCurve.length - 1].equity\n : initialCash;\n\n return {\n fills: this.portfolio.getFills(),\n trades,\n equityCurve,\n initialCash,\n finalEquity,\n metrics: computeRiskMetrics(initialCash, equityCurve, trades),\n };\n }\n\n private fillPendingOrders(bar: OHLCBar): void {\n for (const order of this.pendingOrders) {\n if (order.status !== 'pending') continue;\n const fillPrice = this.resolveFillPrice(order, bar);\n if (fillPrice === null) {\n if (order.timeInForce === 'day' || order.timeInForce === 'ioc') {\n order.status = 'cancelled';\n }\n continue;\n }\n const adjustedPrice = this.slippage.apply(fillPrice, order.side, bar);\n const commission = this.commission.calculate(order.quantity, adjustedPrice);\n const fill: Fill = {\n orderId: order.id,\n time: bar.time,\n price: adjustedPrice,\n quantity: order.quantity,\n side: order.side,\n commission,\n slippage: Math.abs(adjustedPrice - fillPrice),\n tag: order.tag,\n };\n this.portfolio.applyFill(fill);\n order.status = 'filled';\n }\n this.pendingOrders = this.pendingOrders.filter(\n (o) => o.status === 'pending',\n );\n }\n\n private resolveFillPrice(order: PendingOrder, bar: OHLCBar): number | null {\n switch (order.type) {\n case 'market':\n return bar.open;\n case 'limit':\n if (order.price === undefined) return null;\n if (order.side === 'long' && bar.low <= order.price) {\n return Math.min(order.price, bar.open);\n }\n if (order.side === 'short' && bar.high >= order.price) {\n return Math.max(order.price, bar.open);\n }\n return null;\n case 'stop':\n if (order.price === undefined) return null;\n if (order.side === 'long' && bar.high >= order.price) {\n return Math.max(order.price, bar.open);\n }\n if (order.side === 'short' && bar.low <= order.price) {\n return Math.min(order.price, bar.open);\n }\n return null;\n }\n }\n\n private makeContext(\n bar: OHLCBar,\n index: number,\n history: ReadonlyArray<OHLCBar>,\n ): StrategyContext {\n const portfolio = this.portfolio;\n return {\n bar,\n index,\n history,\n position: portfolio.getPosition(),\n cash: portfolio.getCash(),\n equity: portfolio.equity(bar.close),\n placeOrder: (order) => this.placeOrder(order, bar.time),\n close: (tag) => this.closePosition(bar.time, tag),\n cancel: (orderId) => this.cancelOrder(orderId),\n };\n }\n\n private placeOrder(\n raw: Omit<BacktestOrder, 'id'> & { id?: string },\n placedAt: number,\n ): string {\n if (raw.quantity <= 0) {\n throw new Error('order quantity must be > 0');\n }\n if (!this.allowShort && raw.side === 'short') {\n throw new Error('shorting is disabled');\n }\n const id = raw.id ?? `o-${++this.orderSeq}`;\n this.pendingOrders.push({\n id,\n side: raw.side,\n type: raw.type,\n quantity: raw.quantity,\n price: raw.price,\n tag: raw.tag,\n timeInForce: raw.timeInForce ?? 'gtc',\n status: 'pending',\n placedAt,\n });\n return id;\n }\n\n private closePosition(placedAt: number, tag?: string): string | null {\n const pos = this.portfolio.getPosition();\n if (!pos) return null;\n const closeSide: Side = pos.side === 'long' ? 'short' : 'long';\n return this.placeOrder(\n { side: closeSide, type: 'market', quantity: pos.quantity, tag },\n placedAt,\n );\n }\n\n private cancelOrder(orderId: string): boolean {\n const order = this.pendingOrders.find((o) => o.id === orderId);\n if (!order || order.status !== 'pending') return false;\n order.status = 'cancelled';\n return true;\n }\n}\n"],"names":["FixedCommission","perTrade","PercentCommission","rate","quantity","price","PerShareCommission","perShare","minimum","ZERO_COMMISSION","NO_SLIPPAGE","PercentSlippage","intendedPrice","side","adverse","RangeBasedSlippage","factor","bar","push","Portfolio","opts","dir","fill","signed","totalQty","closing","pnl","remaining","time","positionValue","unrealizedPnl","MS_PER_YEAR","computeRiskMetrics","initialCash","equityCurve","trades","emptyMetrics","finalEquity","totalReturn","totalReturnPct","periodsPerYear","inferPeriodsPerYear","periodRiskFree","returns","periodReturns","meanReturn","mean","stdDev","standardDeviation","downsideDev","downsideDeviation","sharpe","sortino","elapsedMs","years","cagr","maxDrawdown","maxDrawdownPct","computeDrawdown","calmar","tradeStats","summarizeTrades","curve","result","prev","values","sum","v","avg","acc","target","count","diff","peak","maxDd","maxDdPct","point","dd","wins","losses","totalWin","totalLoss","t","winRate","averageWin","averageLoss","profitFactor","expectancy","deltas","i","avgMs","perYear","Backtester","data","strategy","ctx","order","fillPrice","adjustedPrice","commission","o","index","history","portfolio","tag","orderId","raw","placedAt","id","pos","closeSide"],"mappings":"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"} | ||
| {"version":3,"file":"index.cjs","sources":["../src/commission.ts","../src/slippage.ts","../src/Portfolio.ts","../src/RiskMetrics.ts","../src/Backtester.ts"],"sourcesContent":["import type { CommissionModel } from './types.js';\n\nexport class FixedCommission implements CommissionModel {\n constructor(private readonly perTrade: number) {}\n\n calculate(): number {\n return this.perTrade;\n }\n}\n\nexport class PercentCommission implements CommissionModel {\n /** rate = 0.001 → 10 bps per trade notional. */\n constructor(private readonly rate: number) {}\n\n calculate(quantity: number, price: number): number {\n return Math.abs(quantity) * price * this.rate;\n }\n}\n\nexport class PerShareCommission implements CommissionModel {\n /** Minimum total commission per trade (optional). */\n constructor(\n private readonly perShare: number,\n private readonly minimum = 0,\n ) {}\n\n calculate(quantity: number): number {\n return Math.max(this.minimum, Math.abs(quantity) * this.perShare);\n }\n}\n\nexport const ZERO_COMMISSION: CommissionModel = {\n calculate: () => 0,\n};\n","import type { OHLCBar } from '@tradecanvas/commons';\nimport type { Side, SlippageModel } from './types.js';\n\nexport const NO_SLIPPAGE: SlippageModel = {\n apply: (price) => price,\n};\n\nexport class PercentSlippage implements SlippageModel {\n /** rate = 0.0005 → 5bps adverse */\n constructor(private readonly rate: number) {}\n\n apply(intendedPrice: number, side: Side): number {\n const adverse = side === 'long' ? 1 + this.rate : 1 - this.rate;\n return intendedPrice * adverse;\n }\n}\n\nexport class RangeBasedSlippage implements SlippageModel {\n /** factor = 0.1 → 10% of the bar's range pushes against the order */\n constructor(private readonly factor: number) {}\n\n apply(intendedPrice: number, side: Side, bar: OHLCBar): number {\n const range = bar.high - bar.low;\n const push = range * this.factor;\n return side === 'long' ? intendedPrice + push : intendedPrice - push;\n }\n}\n","import type {\n ClosedTrade,\n EquityPoint,\n Fill,\n PortfolioPosition,\n Side,\n} from './types.js';\n\nexport interface PortfolioOptions {\n initialCash: number;\n}\n\n/**\n * Tracks cash, a single net position, realized PnL, and the equity curve.\n *\n * Simplifying assumptions:\n * - One symbol at a time. Opposing fills net against the existing position.\n * - Realized PnL is computed when a fill reduces or flips the position.\n * - Equity = cash + position market value (mark-to-market).\n */\nexport class Portfolio {\n private cash: number;\n private readonly initialCash: number;\n private position: PortfolioPosition | null = null;\n private readonly fills: Fill[] = [];\n private readonly trades: ClosedTrade[] = [];\n private readonly equityCurve: EquityPoint[] = [];\n private realizedPnl = 0;\n\n constructor(opts: PortfolioOptions) {\n if (opts.initialCash <= 0) throw new Error('initialCash must be > 0');\n this.cash = opts.initialCash;\n this.initialCash = opts.initialCash;\n }\n\n getCash(): number {\n return this.cash;\n }\n\n getPosition(): Readonly<PortfolioPosition> | null {\n return this.position;\n }\n\n getFills(): ReadonlyArray<Fill> {\n return this.fills;\n }\n\n getTrades(): ReadonlyArray<ClosedTrade> {\n return this.trades;\n }\n\n getEquityCurve(): ReadonlyArray<EquityPoint> {\n return this.equityCurve;\n }\n\n getInitialCash(): number {\n return this.initialCash;\n }\n\n getRealizedPnl(): number {\n return this.realizedPnl;\n }\n\n unrealizedPnl(price: number): number {\n if (!this.position) return 0;\n const dir = this.position.side === 'long' ? 1 : -1;\n return (price - this.position.averagePrice) * this.position.quantity * dir;\n }\n\n equity(price: number): number {\n return this.cash + this.positionValue(price);\n }\n\n positionValue(price: number): number {\n if (!this.position) return 0;\n const dir = this.position.side === 'long' ? 1 : -1;\n return this.position.quantity * price * dir;\n }\n\n /** Apply a fill: cash flow + position update + realized PnL. */\n applyFill(fill: Fill): void {\n this.fills.push(fill);\n\n const signed = fill.side === 'long' ? fill.quantity : -fill.quantity;\n this.cash -= signed * fill.price;\n this.cash -= fill.commission;\n\n if (!this.position) {\n this.position = {\n side: fill.side,\n quantity: fill.quantity,\n averagePrice: fill.price,\n openedAt: fill.time,\n tag: fill.tag,\n };\n return;\n }\n\n if (this.position.side === fill.side) {\n // Same direction: average up\n const totalQty = this.position.quantity + fill.quantity;\n this.position = {\n ...this.position,\n quantity: totalQty,\n averagePrice:\n (this.position.averagePrice * this.position.quantity +\n fill.price * fill.quantity) /\n totalQty,\n };\n return;\n }\n\n // Opposite direction: close or flip\n const closing = Math.min(this.position.quantity, fill.quantity);\n const dir = this.position.side === 'long' ? 1 : -1;\n const pnl = (fill.price - this.position.averagePrice) * closing * dir;\n\n this.realizedPnl += pnl;\n this.trades.push({\n entryTime: this.position.openedAt,\n exitTime: fill.time,\n side: this.position.side,\n quantity: closing,\n entryPrice: this.position.averagePrice,\n exitPrice: fill.price,\n pnl,\n pnlPct: (fill.price / this.position.averagePrice - 1) * dir,\n commission: fill.commission,\n tag: fill.tag ?? this.position.tag,\n });\n\n const remaining = this.position.quantity - fill.quantity;\n if (remaining > 0) {\n this.position = { ...this.position, quantity: remaining };\n } else if (remaining < 0) {\n this.position = {\n side: fill.side,\n quantity: -remaining,\n averagePrice: fill.price,\n openedAt: fill.time,\n tag: fill.tag,\n };\n } else {\n this.position = null;\n }\n }\n\n /** Snapshot equity at the current bar close. */\n mark(time: number, price: number): void {\n const positionValue = this.positionValue(price);\n const unrealizedPnl = this.unrealizedPnl(price);\n this.equityCurve.push({\n time,\n equity: this.cash + positionValue,\n cash: this.cash,\n positionValue,\n unrealizedPnl,\n realizedPnl: this.realizedPnl,\n });\n }\n\n reverseSide(side: Side): Side {\n return side === 'long' ? 'short' : 'long';\n }\n}\n","import type {\n ClosedTrade,\n EquityPoint,\n RiskMetrics,\n} from './types.js';\n\nconst MS_PER_YEAR = 365 * 24 * 60 * 60 * 1000;\n\nexport interface RiskMetricsOptions {\n /** Periods per year for Sharpe/Sortino annualization. Auto-detected from equity timestamps if omitted. */\n periodsPerYear?: number;\n /** Annual risk-free rate, default 0. */\n riskFreeRate?: number;\n}\n\n/**\n * Compute summary risk and return metrics from an equity curve and closed trades.\n *\n * Returns NaN/0 fallbacks for degenerate inputs (empty curve, single point, no losses)\n * so downstream UIs can render safely.\n */\nexport function computeRiskMetrics(\n initialCash: number,\n equityCurve: ReadonlyArray<EquityPoint>,\n trades: ReadonlyArray<ClosedTrade>,\n opts: RiskMetricsOptions = {},\n): RiskMetrics {\n if (equityCurve.length < 2) {\n return emptyMetrics(initialCash, equityCurve, trades);\n }\n\n const finalEquity = equityCurve[equityCurve.length - 1].equity;\n const totalReturn = finalEquity - initialCash;\n const totalReturnPct = totalReturn / initialCash;\n\n const periodsPerYear =\n opts.periodsPerYear ?? inferPeriodsPerYear(equityCurve);\n const riskFreeRate = opts.riskFreeRate ?? 0;\n const periodRiskFree = riskFreeRate / periodsPerYear;\n\n const returns = periodReturns(equityCurve);\n const meanReturn = mean(returns);\n const stdDev = standardDeviation(returns, meanReturn);\n const downsideDev = downsideDeviation(returns, periodRiskFree);\n\n const sharpe = stdDev === 0\n ? 0\n : ((meanReturn - periodRiskFree) / stdDev) * Math.sqrt(periodsPerYear);\n const sortino = downsideDev === 0\n ? 0\n : ((meanReturn - periodRiskFree) / downsideDev) * Math.sqrt(periodsPerYear);\n\n const elapsedMs =\n equityCurve[equityCurve.length - 1].time - equityCurve[0].time;\n const years = elapsedMs > 0 ? elapsedMs / MS_PER_YEAR : 0;\n const cagr = years > 0\n ? Math.pow(finalEquity / initialCash, 1 / years) - 1\n : 0;\n\n const { maxDrawdown, maxDrawdownPct } = computeDrawdown(equityCurve);\n const calmar = maxDrawdownPct > 0 ? cagr / maxDrawdownPct : 0;\n\n const tradeStats = summarizeTrades(trades);\n\n return {\n totalReturn,\n totalReturnPct,\n cagr,\n sharpe,\n sortino,\n calmar,\n maxDrawdown,\n maxDrawdownPct,\n ...tradeStats,\n };\n}\n\nfunction emptyMetrics(\n initialCash: number,\n equityCurve: ReadonlyArray<EquityPoint>,\n trades: ReadonlyArray<ClosedTrade>,\n): RiskMetrics {\n const finalEquity = equityCurve.length > 0\n ? equityCurve[equityCurve.length - 1].equity\n : initialCash;\n return {\n totalReturn: finalEquity - initialCash,\n totalReturnPct: (finalEquity - initialCash) / initialCash,\n cagr: 0,\n sharpe: 0,\n sortino: 0,\n calmar: 0,\n maxDrawdown: 0,\n maxDrawdownPct: 0,\n ...summarizeTrades(trades),\n };\n}\n\nfunction periodReturns(curve: ReadonlyArray<EquityPoint>): number[] {\n const result: number[] = [];\n for (let i = 1; i < curve.length; i++) {\n const prev = curve[i - 1].equity;\n if (prev <= 0) {\n result.push(0);\n continue;\n }\n result.push(curve[i].equity / prev - 1);\n }\n return result;\n}\n\nfunction mean(values: number[]): number {\n if (values.length === 0) return 0;\n let sum = 0;\n for (const v of values) sum += v;\n return sum / values.length;\n}\n\nfunction standardDeviation(values: number[], avg: number): number {\n if (values.length < 2) return 0;\n let acc = 0;\n for (const v of values) acc += (v - avg) ** 2;\n return Math.sqrt(acc / (values.length - 1));\n}\n\nfunction downsideDeviation(values: number[], target: number): number {\n if (values.length < 2) return 0;\n let acc = 0;\n let count = 0;\n for (const v of values) {\n const diff = v - target;\n if (diff < 0) {\n acc += diff ** 2;\n count++;\n }\n }\n if (count === 0) return 0;\n return Math.sqrt(acc / count);\n}\n\nfunction computeDrawdown(curve: ReadonlyArray<EquityPoint>): {\n maxDrawdown: number;\n maxDrawdownPct: number;\n} {\n let peak = curve[0].equity;\n let maxDd = 0;\n let maxDdPct = 0;\n for (const point of curve) {\n if (point.equity > peak) peak = point.equity;\n const dd = peak - point.equity;\n if (dd > maxDd) {\n maxDd = dd;\n maxDdPct = peak > 0 ? dd / peak : 0;\n }\n }\n return { maxDrawdown: maxDd, maxDrawdownPct: maxDdPct };\n}\n\nfunction summarizeTrades(trades: ReadonlyArray<ClosedTrade>): {\n winRate: number;\n profitFactor: number;\n expectancy: number;\n averageWin: number;\n averageLoss: number;\n trades: number;\n} {\n if (trades.length === 0) {\n return {\n winRate: 0,\n profitFactor: 0,\n expectancy: 0,\n averageWin: 0,\n averageLoss: 0,\n trades: 0,\n };\n }\n\n let wins = 0;\n let losses = 0;\n let totalWin = 0;\n let totalLoss = 0;\n for (const t of trades) {\n if (t.pnl > 0) {\n wins++;\n totalWin += t.pnl;\n } else if (t.pnl < 0) {\n losses++;\n totalLoss += -t.pnl;\n }\n }\n\n const winRate = wins / trades.length;\n const averageWin = wins > 0 ? totalWin / wins : 0;\n const averageLoss = losses > 0 ? totalLoss / losses : 0;\n const profitFactor = totalLoss > 0 ? totalWin / totalLoss : totalWin > 0 ? Infinity : 0;\n const expectancy = winRate * averageWin - (1 - winRate) * averageLoss;\n\n return {\n winRate,\n profitFactor,\n expectancy,\n averageWin,\n averageLoss,\n trades: trades.length,\n };\n}\n\nfunction inferPeriodsPerYear(curve: ReadonlyArray<EquityPoint>): number {\n if (curve.length < 2) return 252;\n const deltas: number[] = [];\n for (let i = 1; i < curve.length && i < 50; i++) {\n deltas.push(curve[i].time - curve[i - 1].time);\n }\n const avgMs = mean(deltas);\n if (avgMs <= 0) return 252;\n const perYear = MS_PER_YEAR / avgMs;\n // Snap to common cadences for stability\n if (perYear > 200_000) return 365 * 24 * 60; // 1m\n if (perYear > 50_000) return 365 * 24 * 4; // 15m\n if (perYear > 5_000) return 365 * 24; // hourly\n if (perYear > 200) return 252; // daily trading\n if (perYear > 40) return 52; // weekly\n return 12; // monthly fallback\n}\n","import type { OHLCBar, DataSeries } from '@tradecanvas/commons';\nimport { ZERO_COMMISSION } from './commission.js';\nimport { NO_SLIPPAGE } from './slippage.js';\nimport { Portfolio } from './Portfolio.js';\nimport { computeRiskMetrics } from './RiskMetrics.js';\nimport type {\n BacktestOptions,\n BacktestOrder,\n BacktestResult,\n CommissionModel,\n Fill,\n OrderStatus,\n Side,\n SlippageModel,\n StrategyContext,\n StrategyFn,\n} from './types.js';\n\ninterface PendingOrder extends BacktestOrder {\n status: OrderStatus;\n placedAt: number;\n}\n\n/**\n * Bar-by-bar backtest engine.\n *\n * Execution model:\n * - Strategy fn runs at close of each bar\n * - Orders placed on bar N fill on bar N+1's open (market) or when\n * bar N+1 trades through the limit/stop price\n * - Equity is marked to close of every bar\n *\n * The engine is intentionally headless — no chart dependency — and produces\n * a result that can be visualised via @tradecanvas/chart's EquityCurveRenderer\n * or rendered into a report. See README for wiring examples.\n */\nexport class Backtester {\n private readonly commission: CommissionModel;\n private readonly slippage: SlippageModel;\n private readonly allowShort: boolean;\n private readonly portfolio: Portfolio;\n private pendingOrders: PendingOrder[] = [];\n private orderSeq = 0;\n\n constructor(opts: BacktestOptions) {\n this.commission = opts.commission ?? ZERO_COMMISSION;\n this.slippage = opts.slippage ?? NO_SLIPPAGE;\n this.allowShort = opts.allowShort ?? true;\n this.portfolio = new Portfolio({ initialCash: opts.initialCash });\n }\n\n run(data: DataSeries, strategy: StrategyFn): BacktestResult {\n if (data.length < 2) {\n throw new Error('Backtester requires at least 2 bars');\n }\n\n for (let i = 0; i < data.length; i++) {\n const bar = data[i];\n\n // 1. Fill any pending orders against this bar\n this.fillPendingOrders(bar);\n\n // 2. Mark-to-market after fills resolve\n this.portfolio.mark(bar.time, bar.close);\n\n // 3. Run strategy for the next bar (if there is one)\n if (i < data.length - 1) {\n const ctx = this.makeContext(bar, i, data.slice(0, i + 1));\n strategy(ctx);\n }\n }\n\n // Cancel anything left pending\n for (const order of this.pendingOrders) {\n if (order.status === 'pending') order.status = 'cancelled';\n }\n\n const equityCurve = this.portfolio.getEquityCurve();\n const initialCash = this.portfolio.getInitialCash();\n const trades = this.portfolio.getTrades();\n const finalEquity = equityCurve.length > 0\n ? equityCurve[equityCurve.length - 1].equity\n : initialCash;\n\n return {\n fills: this.portfolio.getFills(),\n trades,\n equityCurve,\n initialCash,\n finalEquity,\n metrics: computeRiskMetrics(initialCash, equityCurve, trades),\n };\n }\n\n private fillPendingOrders(bar: OHLCBar): void {\n for (const order of this.pendingOrders) {\n if (order.status !== 'pending') continue;\n const fillPrice = this.resolveFillPrice(order, bar);\n if (fillPrice === null) {\n if (order.timeInForce === 'day' || order.timeInForce === 'ioc') {\n order.status = 'cancelled';\n }\n continue;\n }\n const adjustedPrice = this.slippage.apply(fillPrice, order.side, bar);\n const commission = this.commission.calculate(order.quantity, adjustedPrice);\n const fill: Fill = {\n orderId: order.id,\n time: bar.time,\n price: adjustedPrice,\n quantity: order.quantity,\n side: order.side,\n commission,\n slippage: Math.abs(adjustedPrice - fillPrice),\n tag: order.tag,\n };\n this.portfolio.applyFill(fill);\n order.status = 'filled';\n }\n this.pendingOrders = this.pendingOrders.filter(\n (o) => o.status === 'pending',\n );\n }\n\n private resolveFillPrice(order: PendingOrder, bar: OHLCBar): number | null {\n switch (order.type) {\n case 'market':\n return bar.open;\n case 'limit':\n if (order.price === undefined) return null;\n if (order.side === 'long' && bar.low <= order.price) {\n return Math.min(order.price, bar.open);\n }\n if (order.side === 'short' && bar.high >= order.price) {\n return Math.max(order.price, bar.open);\n }\n return null;\n case 'stop':\n if (order.price === undefined) return null;\n if (order.side === 'long' && bar.high >= order.price) {\n return Math.max(order.price, bar.open);\n }\n if (order.side === 'short' && bar.low <= order.price) {\n return Math.min(order.price, bar.open);\n }\n return null;\n }\n }\n\n private makeContext(\n bar: OHLCBar,\n index: number,\n history: ReadonlyArray<OHLCBar>,\n ): StrategyContext {\n const portfolio = this.portfolio;\n return {\n bar,\n index,\n history,\n position: portfolio.getPosition(),\n cash: portfolio.getCash(),\n equity: portfolio.equity(bar.close),\n placeOrder: (order) => this.placeOrder(order, bar.time),\n close: (tag) => this.closePosition(bar.time, tag),\n cancel: (orderId) => this.cancelOrder(orderId),\n };\n }\n\n private placeOrder(\n raw: Omit<BacktestOrder, 'id'> & { id?: string },\n placedAt: number,\n ): string {\n if (raw.quantity <= 0) {\n throw new Error('order quantity must be > 0');\n }\n if (!this.allowShort && raw.side === 'short') {\n // `allowShort: false` means \"don't go net short\". A sell that only\n // closes (or reduces) an existing long position is not shorting.\n const pos = this.portfolio.getPosition();\n const closesExistingLong =\n pos !== null && pos.side === 'long' && raw.quantity <= pos.quantity;\n if (!closesExistingLong) {\n throw new Error('shorting is disabled');\n }\n }\n const id = raw.id ?? `o-${++this.orderSeq}`;\n this.pendingOrders.push({\n id,\n side: raw.side,\n type: raw.type,\n quantity: raw.quantity,\n price: raw.price,\n tag: raw.tag,\n timeInForce: raw.timeInForce ?? 'gtc',\n status: 'pending',\n placedAt,\n });\n return id;\n }\n\n private closePosition(placedAt: number, tag?: string): string | null {\n const pos = this.portfolio.getPosition();\n if (!pos) return null;\n const closeSide: Side = pos.side === 'long' ? 'short' : 'long';\n return this.placeOrder(\n { side: closeSide, type: 'market', quantity: pos.quantity, tag },\n placedAt,\n );\n }\n\n private cancelOrder(orderId: string): boolean {\n const order = this.pendingOrders.find((o) => o.id === orderId);\n if (!order || order.status !== 'pending') return false;\n order.status = 'cancelled';\n return true;\n }\n}\n"],"names":["FixedCommission","perTrade","PercentCommission","rate","quantity","price","PerShareCommission","perShare","minimum","ZERO_COMMISSION","NO_SLIPPAGE","PercentSlippage","intendedPrice","side","adverse","RangeBasedSlippage","factor","bar","push","Portfolio","opts","dir","fill","signed","totalQty","closing","pnl","remaining","time","positionValue","unrealizedPnl","MS_PER_YEAR","computeRiskMetrics","initialCash","equityCurve","trades","emptyMetrics","finalEquity","totalReturn","totalReturnPct","periodsPerYear","inferPeriodsPerYear","periodRiskFree","returns","periodReturns","meanReturn","mean","stdDev","standardDeviation","downsideDev","downsideDeviation","sharpe","sortino","elapsedMs","years","cagr","maxDrawdown","maxDrawdownPct","computeDrawdown","calmar","tradeStats","summarizeTrades","curve","result","prev","values","sum","v","avg","acc","target","count","diff","peak","maxDd","maxDdPct","point","dd","wins","losses","totalWin","totalLoss","t","winRate","averageWin","averageLoss","profitFactor","expectancy","deltas","i","avgMs","perYear","Backtester","data","strategy","ctx","order","fillPrice","adjustedPrice","commission","o","index","history","portfolio","tag","orderId","raw","placedAt","pos","id","closeSide"],"mappings":"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"} |
+21
-18
@@ -1,2 +0,2 @@ | ||
| class Y { | ||
| class A { | ||
| constructor(t) { | ||
@@ -9,3 +9,3 @@ this.perTrade = t; | ||
| } | ||
| class L { | ||
| class Y { | ||
| /** rate = 0.001 → 10 bps per trade notional. */ | ||
@@ -30,3 +30,3 @@ constructor(t) { | ||
| calculate: () => 0 | ||
| }, k = { | ||
| }, E = { | ||
| apply: (s) => s | ||
@@ -54,3 +54,3 @@ }; | ||
| } | ||
| class D { | ||
| class k { | ||
| cash; | ||
@@ -163,6 +163,6 @@ initialCash; | ||
| const P = 365 * 24 * 60 * 60 * 1e3; | ||
| function E(s, t, i, e = {}) { | ||
| function D(s, t, i, e = {}) { | ||
| if (t.length < 2) | ||
| return v(s, t, i); | ||
| const n = t[t.length - 1].equity, o = n - s, a = o / s, r = e.periodsPerYear ?? A(t), l = (e.riskFreeRate ?? 0) / r, c = I(t), u = q(c), d = z(c, u), g = C(c, l), M = d === 0 ? 0 : (u - l) / d * Math.sqrt(r), x = g === 0 ? 0 : (u - l) / g * Math.sqrt(r), m = t[t.length - 1].time - t[0].time, f = m > 0 ? m / P : 0, y = f > 0 ? Math.pow(n / s, 1 / f) - 1 : 0, { maxDrawdown: O, maxDrawdownPct: p } = T(t), R = p > 0 ? y / p : 0, S = w(i); | ||
| const n = t[t.length - 1].equity, o = n - s, a = o / s, r = e.periodsPerYear ?? L(t), l = (e.riskFreeRate ?? 0) / r, c = I(t), u = q(c), d = z(c, u), g = C(c, l), x = d === 0 ? 0 : (u - l) / d * Math.sqrt(r), M = g === 0 ? 0 : (u - l) / g * Math.sqrt(r), m = t[t.length - 1].time - t[0].time, f = m > 0 ? m / P : 0, y = f > 0 ? Math.pow(n / s, 1 / f) - 1 : 0, { maxDrawdown: O, maxDrawdownPct: p } = T(t), R = p > 0 ? y / p : 0, S = w(i); | ||
| return { | ||
@@ -172,4 +172,4 @@ totalReturn: o, | ||
| cagr: y, | ||
| sharpe: M, | ||
| sortino: x, | ||
| sharpe: x, | ||
| sortino: M, | ||
| calmar: R, | ||
@@ -260,3 +260,3 @@ maxDrawdown: O, | ||
| } | ||
| function A(s) { | ||
| function L(s) { | ||
| if (s.length < 2) return 252; | ||
@@ -279,3 +279,3 @@ const t = []; | ||
| constructor(t) { | ||
| this.commission = t.commission ?? F, this.slippage = t.slippage ?? k, this.allowShort = t.allowShort ?? !0, this.portfolio = new D({ initialCash: t.initialCash }); | ||
| this.commission = t.commission ?? F, this.slippage = t.slippage ?? E, this.allowShort = t.allowShort ?? !0, this.portfolio = new k({ initialCash: t.initialCash }); | ||
| } | ||
@@ -301,3 +301,3 @@ run(t, i) { | ||
| finalEquity: a, | ||
| metrics: E(n, e, o) | ||
| metrics: D(n, e, o) | ||
| }; | ||
@@ -356,4 +356,7 @@ } | ||
| throw new Error("order quantity must be > 0"); | ||
| if (!this.allowShort && t.side === "short") | ||
| throw new Error("shorting is disabled"); | ||
| if (!this.allowShort && t.side === "short") { | ||
| const n = this.portfolio.getPosition(); | ||
| if (!(n !== null && n.side === "long" && t.quantity <= n.quantity)) | ||
| throw new Error("shorting is disabled"); | ||
| } | ||
| const e = t.id ?? `o-${++this.orderSeq}`; | ||
@@ -388,12 +391,12 @@ return this.pendingOrders.push({ | ||
| W as Backtester, | ||
| Y as FixedCommission, | ||
| k as NO_SLIPPAGE, | ||
| A as FixedCommission, | ||
| E as NO_SLIPPAGE, | ||
| V as PerShareCommission, | ||
| L as PercentCommission, | ||
| Y as PercentCommission, | ||
| _ as PercentSlippage, | ||
| D as Portfolio, | ||
| k as Portfolio, | ||
| B as RangeBasedSlippage, | ||
| F as ZERO_COMMISSION, | ||
| E as computeRiskMetrics | ||
| D as computeRiskMetrics | ||
| }; | ||
| //# sourceMappingURL=index.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","sources":["../src/commission.ts","../src/slippage.ts","../src/Portfolio.ts","../src/RiskMetrics.ts","../src/Backtester.ts"],"sourcesContent":["import type { CommissionModel } from './types.js';\n\nexport class FixedCommission implements CommissionModel {\n constructor(private readonly perTrade: number) {}\n\n calculate(): number {\n return this.perTrade;\n }\n}\n\nexport class PercentCommission implements CommissionModel {\n /** rate = 0.001 → 10 bps per trade notional. */\n constructor(private readonly rate: number) {}\n\n calculate(quantity: number, price: number): number {\n return Math.abs(quantity) * price * this.rate;\n }\n}\n\nexport class PerShareCommission implements CommissionModel {\n /** Minimum total commission per trade (optional). */\n constructor(\n private readonly perShare: number,\n private readonly minimum = 0,\n ) {}\n\n calculate(quantity: number): number {\n return Math.max(this.minimum, Math.abs(quantity) * this.perShare);\n }\n}\n\nexport const ZERO_COMMISSION: CommissionModel = {\n calculate: () => 0,\n};\n","import type { OHLCBar } from '@tradecanvas/commons';\nimport type { Side, SlippageModel } from './types.js';\n\nexport const NO_SLIPPAGE: SlippageModel = {\n apply: (price) => price,\n};\n\nexport class PercentSlippage implements SlippageModel {\n /** rate = 0.0005 → 5bps adverse */\n constructor(private readonly rate: number) {}\n\n apply(intendedPrice: number, side: Side): number {\n const adverse = side === 'long' ? 1 + this.rate : 1 - this.rate;\n return intendedPrice * adverse;\n }\n}\n\nexport class RangeBasedSlippage implements SlippageModel {\n /** factor = 0.1 → 10% of the bar's range pushes against the order */\n constructor(private readonly factor: number) {}\n\n apply(intendedPrice: number, side: Side, bar: OHLCBar): number {\n const range = bar.high - bar.low;\n const push = range * this.factor;\n return side === 'long' ? intendedPrice + push : intendedPrice - push;\n }\n}\n","import type {\n ClosedTrade,\n EquityPoint,\n Fill,\n PortfolioPosition,\n Side,\n} from './types.js';\n\nexport interface PortfolioOptions {\n initialCash: number;\n}\n\n/**\n * Tracks cash, a single net position, realized PnL, and the equity curve.\n *\n * Simplifying assumptions:\n * - One symbol at a time. Opposing fills net against the existing position.\n * - Realized PnL is computed when a fill reduces or flips the position.\n * - Equity = cash + position market value (mark-to-market).\n */\nexport class Portfolio {\n private cash: number;\n private readonly initialCash: number;\n private position: PortfolioPosition | null = null;\n private readonly fills: Fill[] = [];\n private readonly trades: ClosedTrade[] = [];\n private readonly equityCurve: EquityPoint[] = [];\n private realizedPnl = 0;\n\n constructor(opts: PortfolioOptions) {\n if (opts.initialCash <= 0) throw new Error('initialCash must be > 0');\n this.cash = opts.initialCash;\n this.initialCash = opts.initialCash;\n }\n\n getCash(): number {\n return this.cash;\n }\n\n getPosition(): Readonly<PortfolioPosition> | null {\n return this.position;\n }\n\n getFills(): ReadonlyArray<Fill> {\n return this.fills;\n }\n\n getTrades(): ReadonlyArray<ClosedTrade> {\n return this.trades;\n }\n\n getEquityCurve(): ReadonlyArray<EquityPoint> {\n return this.equityCurve;\n }\n\n getInitialCash(): number {\n return this.initialCash;\n }\n\n getRealizedPnl(): number {\n return this.realizedPnl;\n }\n\n unrealizedPnl(price: number): number {\n if (!this.position) return 0;\n const dir = this.position.side === 'long' ? 1 : -1;\n return (price - this.position.averagePrice) * this.position.quantity * dir;\n }\n\n equity(price: number): number {\n return this.cash + this.positionValue(price);\n }\n\n positionValue(price: number): number {\n if (!this.position) return 0;\n const dir = this.position.side === 'long' ? 1 : -1;\n return this.position.quantity * price * dir;\n }\n\n /** Apply a fill: cash flow + position update + realized PnL. */\n applyFill(fill: Fill): void {\n this.fills.push(fill);\n\n const signed = fill.side === 'long' ? fill.quantity : -fill.quantity;\n this.cash -= signed * fill.price;\n this.cash -= fill.commission;\n\n if (!this.position) {\n this.position = {\n side: fill.side,\n quantity: fill.quantity,\n averagePrice: fill.price,\n openedAt: fill.time,\n tag: fill.tag,\n };\n return;\n }\n\n if (this.position.side === fill.side) {\n // Same direction: average up\n const totalQty = this.position.quantity + fill.quantity;\n this.position = {\n ...this.position,\n quantity: totalQty,\n averagePrice:\n (this.position.averagePrice * this.position.quantity +\n fill.price * fill.quantity) /\n totalQty,\n };\n return;\n }\n\n // Opposite direction: close or flip\n const closing = Math.min(this.position.quantity, fill.quantity);\n const dir = this.position.side === 'long' ? 1 : -1;\n const pnl = (fill.price - this.position.averagePrice) * closing * dir;\n\n this.realizedPnl += pnl;\n this.trades.push({\n entryTime: this.position.openedAt,\n exitTime: fill.time,\n side: this.position.side,\n quantity: closing,\n entryPrice: this.position.averagePrice,\n exitPrice: fill.price,\n pnl,\n pnlPct: (fill.price / this.position.averagePrice - 1) * dir,\n commission: fill.commission,\n tag: fill.tag ?? this.position.tag,\n });\n\n const remaining = this.position.quantity - fill.quantity;\n if (remaining > 0) {\n this.position = { ...this.position, quantity: remaining };\n } else if (remaining < 0) {\n this.position = {\n side: fill.side,\n quantity: -remaining,\n averagePrice: fill.price,\n openedAt: fill.time,\n tag: fill.tag,\n };\n } else {\n this.position = null;\n }\n }\n\n /** Snapshot equity at the current bar close. */\n mark(time: number, price: number): void {\n const positionValue = this.positionValue(price);\n const unrealizedPnl = this.unrealizedPnl(price);\n this.equityCurve.push({\n time,\n equity: this.cash + positionValue,\n cash: this.cash,\n positionValue,\n unrealizedPnl,\n realizedPnl: this.realizedPnl,\n });\n }\n\n reverseSide(side: Side): Side {\n return side === 'long' ? 'short' : 'long';\n }\n}\n","import type {\n ClosedTrade,\n EquityPoint,\n RiskMetrics,\n} from './types.js';\n\nconst MS_PER_YEAR = 365 * 24 * 60 * 60 * 1000;\n\nexport interface RiskMetricsOptions {\n /** Periods per year for Sharpe/Sortino annualization. Auto-detected from equity timestamps if omitted. */\n periodsPerYear?: number;\n /** Annual risk-free rate, default 0. */\n riskFreeRate?: number;\n}\n\n/**\n * Compute summary risk and return metrics from an equity curve and closed trades.\n *\n * Returns NaN/0 fallbacks for degenerate inputs (empty curve, single point, no losses)\n * so downstream UIs can render safely.\n */\nexport function computeRiskMetrics(\n initialCash: number,\n equityCurve: ReadonlyArray<EquityPoint>,\n trades: ReadonlyArray<ClosedTrade>,\n opts: RiskMetricsOptions = {},\n): RiskMetrics {\n if (equityCurve.length < 2) {\n return emptyMetrics(initialCash, equityCurve, trades);\n }\n\n const finalEquity = equityCurve[equityCurve.length - 1].equity;\n const totalReturn = finalEquity - initialCash;\n const totalReturnPct = totalReturn / initialCash;\n\n const periodsPerYear =\n opts.periodsPerYear ?? inferPeriodsPerYear(equityCurve);\n const riskFreeRate = opts.riskFreeRate ?? 0;\n const periodRiskFree = riskFreeRate / periodsPerYear;\n\n const returns = periodReturns(equityCurve);\n const meanReturn = mean(returns);\n const stdDev = standardDeviation(returns, meanReturn);\n const downsideDev = downsideDeviation(returns, periodRiskFree);\n\n const sharpe = stdDev === 0\n ? 0\n : ((meanReturn - periodRiskFree) / stdDev) * Math.sqrt(periodsPerYear);\n const sortino = downsideDev === 0\n ? 0\n : ((meanReturn - periodRiskFree) / downsideDev) * Math.sqrt(periodsPerYear);\n\n const elapsedMs =\n equityCurve[equityCurve.length - 1].time - equityCurve[0].time;\n const years = elapsedMs > 0 ? elapsedMs / MS_PER_YEAR : 0;\n const cagr = years > 0\n ? Math.pow(finalEquity / initialCash, 1 / years) - 1\n : 0;\n\n const { maxDrawdown, maxDrawdownPct } = computeDrawdown(equityCurve);\n const calmar = maxDrawdownPct > 0 ? cagr / maxDrawdownPct : 0;\n\n const tradeStats = summarizeTrades(trades);\n\n return {\n totalReturn,\n totalReturnPct,\n cagr,\n sharpe,\n sortino,\n calmar,\n maxDrawdown,\n maxDrawdownPct,\n ...tradeStats,\n };\n}\n\nfunction emptyMetrics(\n initialCash: number,\n equityCurve: ReadonlyArray<EquityPoint>,\n trades: ReadonlyArray<ClosedTrade>,\n): RiskMetrics {\n const finalEquity = equityCurve.length > 0\n ? equityCurve[equityCurve.length - 1].equity\n : initialCash;\n return {\n totalReturn: finalEquity - initialCash,\n totalReturnPct: (finalEquity - initialCash) / initialCash,\n cagr: 0,\n sharpe: 0,\n sortino: 0,\n calmar: 0,\n maxDrawdown: 0,\n maxDrawdownPct: 0,\n ...summarizeTrades(trades),\n };\n}\n\nfunction periodReturns(curve: ReadonlyArray<EquityPoint>): number[] {\n const result: number[] = [];\n for (let i = 1; i < curve.length; i++) {\n const prev = curve[i - 1].equity;\n if (prev <= 0) {\n result.push(0);\n continue;\n }\n result.push(curve[i].equity / prev - 1);\n }\n return result;\n}\n\nfunction mean(values: number[]): number {\n if (values.length === 0) return 0;\n let sum = 0;\n for (const v of values) sum += v;\n return sum / values.length;\n}\n\nfunction standardDeviation(values: number[], avg: number): number {\n if (values.length < 2) return 0;\n let acc = 0;\n for (const v of values) acc += (v - avg) ** 2;\n return Math.sqrt(acc / (values.length - 1));\n}\n\nfunction downsideDeviation(values: number[], target: number): number {\n if (values.length < 2) return 0;\n let acc = 0;\n let count = 0;\n for (const v of values) {\n const diff = v - target;\n if (diff < 0) {\n acc += diff ** 2;\n count++;\n }\n }\n if (count === 0) return 0;\n return Math.sqrt(acc / count);\n}\n\nfunction computeDrawdown(curve: ReadonlyArray<EquityPoint>): {\n maxDrawdown: number;\n maxDrawdownPct: number;\n} {\n let peak = curve[0].equity;\n let maxDd = 0;\n let maxDdPct = 0;\n for (const point of curve) {\n if (point.equity > peak) peak = point.equity;\n const dd = peak - point.equity;\n if (dd > maxDd) {\n maxDd = dd;\n maxDdPct = peak > 0 ? dd / peak : 0;\n }\n }\n return { maxDrawdown: maxDd, maxDrawdownPct: maxDdPct };\n}\n\nfunction summarizeTrades(trades: ReadonlyArray<ClosedTrade>): {\n winRate: number;\n profitFactor: number;\n expectancy: number;\n averageWin: number;\n averageLoss: number;\n trades: number;\n} {\n if (trades.length === 0) {\n return {\n winRate: 0,\n profitFactor: 0,\n expectancy: 0,\n averageWin: 0,\n averageLoss: 0,\n trades: 0,\n };\n }\n\n let wins = 0;\n let losses = 0;\n let totalWin = 0;\n let totalLoss = 0;\n for (const t of trades) {\n if (t.pnl > 0) {\n wins++;\n totalWin += t.pnl;\n } else if (t.pnl < 0) {\n losses++;\n totalLoss += -t.pnl;\n }\n }\n\n const winRate = wins / trades.length;\n const averageWin = wins > 0 ? totalWin / wins : 0;\n const averageLoss = losses > 0 ? totalLoss / losses : 0;\n const profitFactor = totalLoss > 0 ? totalWin / totalLoss : totalWin > 0 ? Infinity : 0;\n const expectancy = winRate * averageWin - (1 - winRate) * averageLoss;\n\n return {\n winRate,\n profitFactor,\n expectancy,\n averageWin,\n averageLoss,\n trades: trades.length,\n };\n}\n\nfunction inferPeriodsPerYear(curve: ReadonlyArray<EquityPoint>): number {\n if (curve.length < 2) return 252;\n const deltas: number[] = [];\n for (let i = 1; i < curve.length && i < 50; i++) {\n deltas.push(curve[i].time - curve[i - 1].time);\n }\n const avgMs = mean(deltas);\n if (avgMs <= 0) return 252;\n const perYear = MS_PER_YEAR / avgMs;\n // Snap to common cadences for stability\n if (perYear > 200_000) return 365 * 24 * 60; // 1m\n if (perYear > 50_000) return 365 * 24 * 4; // 15m\n if (perYear > 5_000) return 365 * 24; // hourly\n if (perYear > 200) return 252; // daily trading\n if (perYear > 40) return 52; // weekly\n return 12; // monthly fallback\n}\n","import type { OHLCBar, DataSeries } from '@tradecanvas/commons';\nimport { ZERO_COMMISSION } from './commission.js';\nimport { NO_SLIPPAGE } from './slippage.js';\nimport { Portfolio } from './Portfolio.js';\nimport { computeRiskMetrics } from './RiskMetrics.js';\nimport type {\n BacktestOptions,\n BacktestOrder,\n BacktestResult,\n CommissionModel,\n Fill,\n OrderStatus,\n Side,\n SlippageModel,\n StrategyContext,\n StrategyFn,\n} from './types.js';\n\ninterface PendingOrder extends BacktestOrder {\n status: OrderStatus;\n placedAt: number;\n}\n\n/**\n * Bar-by-bar backtest engine.\n *\n * Execution model:\n * - Strategy fn runs at close of each bar\n * - Orders placed on bar N fill on bar N+1's open (market) or when\n * bar N+1 trades through the limit/stop price\n * - Equity is marked to close of every bar\n *\n * The engine is intentionally headless — no chart dependency — and produces\n * a result that can be visualised via @tradecanvas/chart's EquityCurveRenderer\n * or rendered into a report. See README for wiring examples.\n */\nexport class Backtester {\n private readonly commission: CommissionModel;\n private readonly slippage: SlippageModel;\n private readonly allowShort: boolean;\n private readonly portfolio: Portfolio;\n private pendingOrders: PendingOrder[] = [];\n private orderSeq = 0;\n\n constructor(opts: BacktestOptions) {\n this.commission = opts.commission ?? ZERO_COMMISSION;\n this.slippage = opts.slippage ?? NO_SLIPPAGE;\n this.allowShort = opts.allowShort ?? true;\n this.portfolio = new Portfolio({ initialCash: opts.initialCash });\n }\n\n run(data: DataSeries, strategy: StrategyFn): BacktestResult {\n if (data.length < 2) {\n throw new Error('Backtester requires at least 2 bars');\n }\n\n for (let i = 0; i < data.length; i++) {\n const bar = data[i];\n\n // 1. Fill any pending orders against this bar\n this.fillPendingOrders(bar);\n\n // 2. Mark-to-market after fills resolve\n this.portfolio.mark(bar.time, bar.close);\n\n // 3. Run strategy for the next bar (if there is one)\n if (i < data.length - 1) {\n const ctx = this.makeContext(bar, i, data.slice(0, i + 1));\n strategy(ctx);\n }\n }\n\n // Cancel anything left pending\n for (const order of this.pendingOrders) {\n if (order.status === 'pending') order.status = 'cancelled';\n }\n\n const equityCurve = this.portfolio.getEquityCurve();\n const initialCash = this.portfolio.getInitialCash();\n const trades = this.portfolio.getTrades();\n const finalEquity = equityCurve.length > 0\n ? equityCurve[equityCurve.length - 1].equity\n : initialCash;\n\n return {\n fills: this.portfolio.getFills(),\n trades,\n equityCurve,\n initialCash,\n finalEquity,\n metrics: computeRiskMetrics(initialCash, equityCurve, trades),\n };\n }\n\n private fillPendingOrders(bar: OHLCBar): void {\n for (const order of this.pendingOrders) {\n if (order.status !== 'pending') continue;\n const fillPrice = this.resolveFillPrice(order, bar);\n if (fillPrice === null) {\n if (order.timeInForce === 'day' || order.timeInForce === 'ioc') {\n order.status = 'cancelled';\n }\n continue;\n }\n const adjustedPrice = this.slippage.apply(fillPrice, order.side, bar);\n const commission = this.commission.calculate(order.quantity, adjustedPrice);\n const fill: Fill = {\n orderId: order.id,\n time: bar.time,\n price: adjustedPrice,\n quantity: order.quantity,\n side: order.side,\n commission,\n slippage: Math.abs(adjustedPrice - fillPrice),\n tag: order.tag,\n };\n this.portfolio.applyFill(fill);\n order.status = 'filled';\n }\n this.pendingOrders = this.pendingOrders.filter(\n (o) => o.status === 'pending',\n );\n }\n\n private resolveFillPrice(order: PendingOrder, bar: OHLCBar): number | null {\n switch (order.type) {\n case 'market':\n return bar.open;\n case 'limit':\n if (order.price === undefined) return null;\n if (order.side === 'long' && bar.low <= order.price) {\n return Math.min(order.price, bar.open);\n }\n if (order.side === 'short' && bar.high >= order.price) {\n return Math.max(order.price, bar.open);\n }\n return null;\n case 'stop':\n if (order.price === undefined) return null;\n if (order.side === 'long' && bar.high >= order.price) {\n return Math.max(order.price, bar.open);\n }\n if (order.side === 'short' && bar.low <= order.price) {\n return Math.min(order.price, bar.open);\n }\n return null;\n }\n }\n\n private makeContext(\n bar: OHLCBar,\n index: number,\n history: ReadonlyArray<OHLCBar>,\n ): StrategyContext {\n const portfolio = this.portfolio;\n return {\n bar,\n index,\n history,\n position: portfolio.getPosition(),\n cash: portfolio.getCash(),\n equity: portfolio.equity(bar.close),\n placeOrder: (order) => this.placeOrder(order, bar.time),\n close: (tag) => this.closePosition(bar.time, tag),\n cancel: (orderId) => this.cancelOrder(orderId),\n };\n }\n\n private placeOrder(\n raw: Omit<BacktestOrder, 'id'> & { id?: string },\n placedAt: number,\n ): string {\n if (raw.quantity <= 0) {\n throw new Error('order quantity must be > 0');\n }\n if (!this.allowShort && raw.side === 'short') {\n throw new Error('shorting is disabled');\n }\n const id = raw.id ?? `o-${++this.orderSeq}`;\n this.pendingOrders.push({\n id,\n side: raw.side,\n type: raw.type,\n quantity: raw.quantity,\n price: raw.price,\n tag: raw.tag,\n timeInForce: raw.timeInForce ?? 'gtc',\n status: 'pending',\n placedAt,\n });\n return id;\n }\n\n private closePosition(placedAt: number, tag?: string): string | null {\n const pos = this.portfolio.getPosition();\n if (!pos) return null;\n const closeSide: Side = pos.side === 'long' ? 'short' : 'long';\n return this.placeOrder(\n { side: closeSide, type: 'market', quantity: pos.quantity, tag },\n placedAt,\n );\n }\n\n private cancelOrder(orderId: string): boolean {\n const order = this.pendingOrders.find((o) => o.id === orderId);\n if (!order || order.status !== 'pending') return false;\n order.status = 'cancelled';\n return true;\n }\n}\n"],"names":["FixedCommission","perTrade","PercentCommission","rate","quantity","price","PerShareCommission","perShare","minimum","ZERO_COMMISSION","NO_SLIPPAGE","PercentSlippage","intendedPrice","side","adverse","RangeBasedSlippage","factor","bar","push","Portfolio","opts","dir","fill","signed","totalQty","closing","pnl","remaining","time","positionValue","unrealizedPnl","MS_PER_YEAR","computeRiskMetrics","initialCash","equityCurve","trades","emptyMetrics","finalEquity","totalReturn","totalReturnPct","periodsPerYear","inferPeriodsPerYear","periodRiskFree","returns","periodReturns","meanReturn","mean","stdDev","standardDeviation","downsideDev","downsideDeviation","sharpe","sortino","elapsedMs","years","cagr","maxDrawdown","maxDrawdownPct","computeDrawdown","calmar","tradeStats","summarizeTrades","curve","result","prev","values","sum","v","avg","acc","target","count","diff","peak","maxDd","maxDdPct","point","dd","wins","losses","totalWin","totalLoss","t","winRate","averageWin","averageLoss","profitFactor","expectancy","deltas","i","avgMs","perYear","Backtester","data","strategy","ctx","order","fillPrice","adjustedPrice","commission","o","index","history","portfolio","tag","orderId","raw","placedAt","id","pos","closeSide"],"mappings":"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"} | ||
| {"version":3,"file":"index.js","sources":["../src/commission.ts","../src/slippage.ts","../src/Portfolio.ts","../src/RiskMetrics.ts","../src/Backtester.ts"],"sourcesContent":["import type { CommissionModel } from './types.js';\n\nexport class FixedCommission implements CommissionModel {\n constructor(private readonly perTrade: number) {}\n\n calculate(): number {\n return this.perTrade;\n }\n}\n\nexport class PercentCommission implements CommissionModel {\n /** rate = 0.001 → 10 bps per trade notional. */\n constructor(private readonly rate: number) {}\n\n calculate(quantity: number, price: number): number {\n return Math.abs(quantity) * price * this.rate;\n }\n}\n\nexport class PerShareCommission implements CommissionModel {\n /** Minimum total commission per trade (optional). */\n constructor(\n private readonly perShare: number,\n private readonly minimum = 0,\n ) {}\n\n calculate(quantity: number): number {\n return Math.max(this.minimum, Math.abs(quantity) * this.perShare);\n }\n}\n\nexport const ZERO_COMMISSION: CommissionModel = {\n calculate: () => 0,\n};\n","import type { OHLCBar } from '@tradecanvas/commons';\nimport type { Side, SlippageModel } from './types.js';\n\nexport const NO_SLIPPAGE: SlippageModel = {\n apply: (price) => price,\n};\n\nexport class PercentSlippage implements SlippageModel {\n /** rate = 0.0005 → 5bps adverse */\n constructor(private readonly rate: number) {}\n\n apply(intendedPrice: number, side: Side): number {\n const adverse = side === 'long' ? 1 + this.rate : 1 - this.rate;\n return intendedPrice * adverse;\n }\n}\n\nexport class RangeBasedSlippage implements SlippageModel {\n /** factor = 0.1 → 10% of the bar's range pushes against the order */\n constructor(private readonly factor: number) {}\n\n apply(intendedPrice: number, side: Side, bar: OHLCBar): number {\n const range = bar.high - bar.low;\n const push = range * this.factor;\n return side === 'long' ? intendedPrice + push : intendedPrice - push;\n }\n}\n","import type {\n ClosedTrade,\n EquityPoint,\n Fill,\n PortfolioPosition,\n Side,\n} from './types.js';\n\nexport interface PortfolioOptions {\n initialCash: number;\n}\n\n/**\n * Tracks cash, a single net position, realized PnL, and the equity curve.\n *\n * Simplifying assumptions:\n * - One symbol at a time. Opposing fills net against the existing position.\n * - Realized PnL is computed when a fill reduces or flips the position.\n * - Equity = cash + position market value (mark-to-market).\n */\nexport class Portfolio {\n private cash: number;\n private readonly initialCash: number;\n private position: PortfolioPosition | null = null;\n private readonly fills: Fill[] = [];\n private readonly trades: ClosedTrade[] = [];\n private readonly equityCurve: EquityPoint[] = [];\n private realizedPnl = 0;\n\n constructor(opts: PortfolioOptions) {\n if (opts.initialCash <= 0) throw new Error('initialCash must be > 0');\n this.cash = opts.initialCash;\n this.initialCash = opts.initialCash;\n }\n\n getCash(): number {\n return this.cash;\n }\n\n getPosition(): Readonly<PortfolioPosition> | null {\n return this.position;\n }\n\n getFills(): ReadonlyArray<Fill> {\n return this.fills;\n }\n\n getTrades(): ReadonlyArray<ClosedTrade> {\n return this.trades;\n }\n\n getEquityCurve(): ReadonlyArray<EquityPoint> {\n return this.equityCurve;\n }\n\n getInitialCash(): number {\n return this.initialCash;\n }\n\n getRealizedPnl(): number {\n return this.realizedPnl;\n }\n\n unrealizedPnl(price: number): number {\n if (!this.position) return 0;\n const dir = this.position.side === 'long' ? 1 : -1;\n return (price - this.position.averagePrice) * this.position.quantity * dir;\n }\n\n equity(price: number): number {\n return this.cash + this.positionValue(price);\n }\n\n positionValue(price: number): number {\n if (!this.position) return 0;\n const dir = this.position.side === 'long' ? 1 : -1;\n return this.position.quantity * price * dir;\n }\n\n /** Apply a fill: cash flow + position update + realized PnL. */\n applyFill(fill: Fill): void {\n this.fills.push(fill);\n\n const signed = fill.side === 'long' ? fill.quantity : -fill.quantity;\n this.cash -= signed * fill.price;\n this.cash -= fill.commission;\n\n if (!this.position) {\n this.position = {\n side: fill.side,\n quantity: fill.quantity,\n averagePrice: fill.price,\n openedAt: fill.time,\n tag: fill.tag,\n };\n return;\n }\n\n if (this.position.side === fill.side) {\n // Same direction: average up\n const totalQty = this.position.quantity + fill.quantity;\n this.position = {\n ...this.position,\n quantity: totalQty,\n averagePrice:\n (this.position.averagePrice * this.position.quantity +\n fill.price * fill.quantity) /\n totalQty,\n };\n return;\n }\n\n // Opposite direction: close or flip\n const closing = Math.min(this.position.quantity, fill.quantity);\n const dir = this.position.side === 'long' ? 1 : -1;\n const pnl = (fill.price - this.position.averagePrice) * closing * dir;\n\n this.realizedPnl += pnl;\n this.trades.push({\n entryTime: this.position.openedAt,\n exitTime: fill.time,\n side: this.position.side,\n quantity: closing,\n entryPrice: this.position.averagePrice,\n exitPrice: fill.price,\n pnl,\n pnlPct: (fill.price / this.position.averagePrice - 1) * dir,\n commission: fill.commission,\n tag: fill.tag ?? this.position.tag,\n });\n\n const remaining = this.position.quantity - fill.quantity;\n if (remaining > 0) {\n this.position = { ...this.position, quantity: remaining };\n } else if (remaining < 0) {\n this.position = {\n side: fill.side,\n quantity: -remaining,\n averagePrice: fill.price,\n openedAt: fill.time,\n tag: fill.tag,\n };\n } else {\n this.position = null;\n }\n }\n\n /** Snapshot equity at the current bar close. */\n mark(time: number, price: number): void {\n const positionValue = this.positionValue(price);\n const unrealizedPnl = this.unrealizedPnl(price);\n this.equityCurve.push({\n time,\n equity: this.cash + positionValue,\n cash: this.cash,\n positionValue,\n unrealizedPnl,\n realizedPnl: this.realizedPnl,\n });\n }\n\n reverseSide(side: Side): Side {\n return side === 'long' ? 'short' : 'long';\n }\n}\n","import type {\n ClosedTrade,\n EquityPoint,\n RiskMetrics,\n} from './types.js';\n\nconst MS_PER_YEAR = 365 * 24 * 60 * 60 * 1000;\n\nexport interface RiskMetricsOptions {\n /** Periods per year for Sharpe/Sortino annualization. Auto-detected from equity timestamps if omitted. */\n periodsPerYear?: number;\n /** Annual risk-free rate, default 0. */\n riskFreeRate?: number;\n}\n\n/**\n * Compute summary risk and return metrics from an equity curve and closed trades.\n *\n * Returns NaN/0 fallbacks for degenerate inputs (empty curve, single point, no losses)\n * so downstream UIs can render safely.\n */\nexport function computeRiskMetrics(\n initialCash: number,\n equityCurve: ReadonlyArray<EquityPoint>,\n trades: ReadonlyArray<ClosedTrade>,\n opts: RiskMetricsOptions = {},\n): RiskMetrics {\n if (equityCurve.length < 2) {\n return emptyMetrics(initialCash, equityCurve, trades);\n }\n\n const finalEquity = equityCurve[equityCurve.length - 1].equity;\n const totalReturn = finalEquity - initialCash;\n const totalReturnPct = totalReturn / initialCash;\n\n const periodsPerYear =\n opts.periodsPerYear ?? inferPeriodsPerYear(equityCurve);\n const riskFreeRate = opts.riskFreeRate ?? 0;\n const periodRiskFree = riskFreeRate / periodsPerYear;\n\n const returns = periodReturns(equityCurve);\n const meanReturn = mean(returns);\n const stdDev = standardDeviation(returns, meanReturn);\n const downsideDev = downsideDeviation(returns, periodRiskFree);\n\n const sharpe = stdDev === 0\n ? 0\n : ((meanReturn - periodRiskFree) / stdDev) * Math.sqrt(periodsPerYear);\n const sortino = downsideDev === 0\n ? 0\n : ((meanReturn - periodRiskFree) / downsideDev) * Math.sqrt(periodsPerYear);\n\n const elapsedMs =\n equityCurve[equityCurve.length - 1].time - equityCurve[0].time;\n const years = elapsedMs > 0 ? elapsedMs / MS_PER_YEAR : 0;\n const cagr = years > 0\n ? Math.pow(finalEquity / initialCash, 1 / years) - 1\n : 0;\n\n const { maxDrawdown, maxDrawdownPct } = computeDrawdown(equityCurve);\n const calmar = maxDrawdownPct > 0 ? cagr / maxDrawdownPct : 0;\n\n const tradeStats = summarizeTrades(trades);\n\n return {\n totalReturn,\n totalReturnPct,\n cagr,\n sharpe,\n sortino,\n calmar,\n maxDrawdown,\n maxDrawdownPct,\n ...tradeStats,\n };\n}\n\nfunction emptyMetrics(\n initialCash: number,\n equityCurve: ReadonlyArray<EquityPoint>,\n trades: ReadonlyArray<ClosedTrade>,\n): RiskMetrics {\n const finalEquity = equityCurve.length > 0\n ? equityCurve[equityCurve.length - 1].equity\n : initialCash;\n return {\n totalReturn: finalEquity - initialCash,\n totalReturnPct: (finalEquity - initialCash) / initialCash,\n cagr: 0,\n sharpe: 0,\n sortino: 0,\n calmar: 0,\n maxDrawdown: 0,\n maxDrawdownPct: 0,\n ...summarizeTrades(trades),\n };\n}\n\nfunction periodReturns(curve: ReadonlyArray<EquityPoint>): number[] {\n const result: number[] = [];\n for (let i = 1; i < curve.length; i++) {\n const prev = curve[i - 1].equity;\n if (prev <= 0) {\n result.push(0);\n continue;\n }\n result.push(curve[i].equity / prev - 1);\n }\n return result;\n}\n\nfunction mean(values: number[]): number {\n if (values.length === 0) return 0;\n let sum = 0;\n for (const v of values) sum += v;\n return sum / values.length;\n}\n\nfunction standardDeviation(values: number[], avg: number): number {\n if (values.length < 2) return 0;\n let acc = 0;\n for (const v of values) acc += (v - avg) ** 2;\n return Math.sqrt(acc / (values.length - 1));\n}\n\nfunction downsideDeviation(values: number[], target: number): number {\n if (values.length < 2) return 0;\n let acc = 0;\n let count = 0;\n for (const v of values) {\n const diff = v - target;\n if (diff < 0) {\n acc += diff ** 2;\n count++;\n }\n }\n if (count === 0) return 0;\n return Math.sqrt(acc / count);\n}\n\nfunction computeDrawdown(curve: ReadonlyArray<EquityPoint>): {\n maxDrawdown: number;\n maxDrawdownPct: number;\n} {\n let peak = curve[0].equity;\n let maxDd = 0;\n let maxDdPct = 0;\n for (const point of curve) {\n if (point.equity > peak) peak = point.equity;\n const dd = peak - point.equity;\n if (dd > maxDd) {\n maxDd = dd;\n maxDdPct = peak > 0 ? dd / peak : 0;\n }\n }\n return { maxDrawdown: maxDd, maxDrawdownPct: maxDdPct };\n}\n\nfunction summarizeTrades(trades: ReadonlyArray<ClosedTrade>): {\n winRate: number;\n profitFactor: number;\n expectancy: number;\n averageWin: number;\n averageLoss: number;\n trades: number;\n} {\n if (trades.length === 0) {\n return {\n winRate: 0,\n profitFactor: 0,\n expectancy: 0,\n averageWin: 0,\n averageLoss: 0,\n trades: 0,\n };\n }\n\n let wins = 0;\n let losses = 0;\n let totalWin = 0;\n let totalLoss = 0;\n for (const t of trades) {\n if (t.pnl > 0) {\n wins++;\n totalWin += t.pnl;\n } else if (t.pnl < 0) {\n losses++;\n totalLoss += -t.pnl;\n }\n }\n\n const winRate = wins / trades.length;\n const averageWin = wins > 0 ? totalWin / wins : 0;\n const averageLoss = losses > 0 ? totalLoss / losses : 0;\n const profitFactor = totalLoss > 0 ? totalWin / totalLoss : totalWin > 0 ? Infinity : 0;\n const expectancy = winRate * averageWin - (1 - winRate) * averageLoss;\n\n return {\n winRate,\n profitFactor,\n expectancy,\n averageWin,\n averageLoss,\n trades: trades.length,\n };\n}\n\nfunction inferPeriodsPerYear(curve: ReadonlyArray<EquityPoint>): number {\n if (curve.length < 2) return 252;\n const deltas: number[] = [];\n for (let i = 1; i < curve.length && i < 50; i++) {\n deltas.push(curve[i].time - curve[i - 1].time);\n }\n const avgMs = mean(deltas);\n if (avgMs <= 0) return 252;\n const perYear = MS_PER_YEAR / avgMs;\n // Snap to common cadences for stability\n if (perYear > 200_000) return 365 * 24 * 60; // 1m\n if (perYear > 50_000) return 365 * 24 * 4; // 15m\n if (perYear > 5_000) return 365 * 24; // hourly\n if (perYear > 200) return 252; // daily trading\n if (perYear > 40) return 52; // weekly\n return 12; // monthly fallback\n}\n","import type { OHLCBar, DataSeries } from '@tradecanvas/commons';\nimport { ZERO_COMMISSION } from './commission.js';\nimport { NO_SLIPPAGE } from './slippage.js';\nimport { Portfolio } from './Portfolio.js';\nimport { computeRiskMetrics } from './RiskMetrics.js';\nimport type {\n BacktestOptions,\n BacktestOrder,\n BacktestResult,\n CommissionModel,\n Fill,\n OrderStatus,\n Side,\n SlippageModel,\n StrategyContext,\n StrategyFn,\n} from './types.js';\n\ninterface PendingOrder extends BacktestOrder {\n status: OrderStatus;\n placedAt: number;\n}\n\n/**\n * Bar-by-bar backtest engine.\n *\n * Execution model:\n * - Strategy fn runs at close of each bar\n * - Orders placed on bar N fill on bar N+1's open (market) or when\n * bar N+1 trades through the limit/stop price\n * - Equity is marked to close of every bar\n *\n * The engine is intentionally headless — no chart dependency — and produces\n * a result that can be visualised via @tradecanvas/chart's EquityCurveRenderer\n * or rendered into a report. See README for wiring examples.\n */\nexport class Backtester {\n private readonly commission: CommissionModel;\n private readonly slippage: SlippageModel;\n private readonly allowShort: boolean;\n private readonly portfolio: Portfolio;\n private pendingOrders: PendingOrder[] = [];\n private orderSeq = 0;\n\n constructor(opts: BacktestOptions) {\n this.commission = opts.commission ?? ZERO_COMMISSION;\n this.slippage = opts.slippage ?? NO_SLIPPAGE;\n this.allowShort = opts.allowShort ?? true;\n this.portfolio = new Portfolio({ initialCash: opts.initialCash });\n }\n\n run(data: DataSeries, strategy: StrategyFn): BacktestResult {\n if (data.length < 2) {\n throw new Error('Backtester requires at least 2 bars');\n }\n\n for (let i = 0; i < data.length; i++) {\n const bar = data[i];\n\n // 1. Fill any pending orders against this bar\n this.fillPendingOrders(bar);\n\n // 2. Mark-to-market after fills resolve\n this.portfolio.mark(bar.time, bar.close);\n\n // 3. Run strategy for the next bar (if there is one)\n if (i < data.length - 1) {\n const ctx = this.makeContext(bar, i, data.slice(0, i + 1));\n strategy(ctx);\n }\n }\n\n // Cancel anything left pending\n for (const order of this.pendingOrders) {\n if (order.status === 'pending') order.status = 'cancelled';\n }\n\n const equityCurve = this.portfolio.getEquityCurve();\n const initialCash = this.portfolio.getInitialCash();\n const trades = this.portfolio.getTrades();\n const finalEquity = equityCurve.length > 0\n ? equityCurve[equityCurve.length - 1].equity\n : initialCash;\n\n return {\n fills: this.portfolio.getFills(),\n trades,\n equityCurve,\n initialCash,\n finalEquity,\n metrics: computeRiskMetrics(initialCash, equityCurve, trades),\n };\n }\n\n private fillPendingOrders(bar: OHLCBar): void {\n for (const order of this.pendingOrders) {\n if (order.status !== 'pending') continue;\n const fillPrice = this.resolveFillPrice(order, bar);\n if (fillPrice === null) {\n if (order.timeInForce === 'day' || order.timeInForce === 'ioc') {\n order.status = 'cancelled';\n }\n continue;\n }\n const adjustedPrice = this.slippage.apply(fillPrice, order.side, bar);\n const commission = this.commission.calculate(order.quantity, adjustedPrice);\n const fill: Fill = {\n orderId: order.id,\n time: bar.time,\n price: adjustedPrice,\n quantity: order.quantity,\n side: order.side,\n commission,\n slippage: Math.abs(adjustedPrice - fillPrice),\n tag: order.tag,\n };\n this.portfolio.applyFill(fill);\n order.status = 'filled';\n }\n this.pendingOrders = this.pendingOrders.filter(\n (o) => o.status === 'pending',\n );\n }\n\n private resolveFillPrice(order: PendingOrder, bar: OHLCBar): number | null {\n switch (order.type) {\n case 'market':\n return bar.open;\n case 'limit':\n if (order.price === undefined) return null;\n if (order.side === 'long' && bar.low <= order.price) {\n return Math.min(order.price, bar.open);\n }\n if (order.side === 'short' && bar.high >= order.price) {\n return Math.max(order.price, bar.open);\n }\n return null;\n case 'stop':\n if (order.price === undefined) return null;\n if (order.side === 'long' && bar.high >= order.price) {\n return Math.max(order.price, bar.open);\n }\n if (order.side === 'short' && bar.low <= order.price) {\n return Math.min(order.price, bar.open);\n }\n return null;\n }\n }\n\n private makeContext(\n bar: OHLCBar,\n index: number,\n history: ReadonlyArray<OHLCBar>,\n ): StrategyContext {\n const portfolio = this.portfolio;\n return {\n bar,\n index,\n history,\n position: portfolio.getPosition(),\n cash: portfolio.getCash(),\n equity: portfolio.equity(bar.close),\n placeOrder: (order) => this.placeOrder(order, bar.time),\n close: (tag) => this.closePosition(bar.time, tag),\n cancel: (orderId) => this.cancelOrder(orderId),\n };\n }\n\n private placeOrder(\n raw: Omit<BacktestOrder, 'id'> & { id?: string },\n placedAt: number,\n ): string {\n if (raw.quantity <= 0) {\n throw new Error('order quantity must be > 0');\n }\n if (!this.allowShort && raw.side === 'short') {\n // `allowShort: false` means \"don't go net short\". A sell that only\n // closes (or reduces) an existing long position is not shorting.\n const pos = this.portfolio.getPosition();\n const closesExistingLong =\n pos !== null && pos.side === 'long' && raw.quantity <= pos.quantity;\n if (!closesExistingLong) {\n throw new Error('shorting is disabled');\n }\n }\n const id = raw.id ?? `o-${++this.orderSeq}`;\n this.pendingOrders.push({\n id,\n side: raw.side,\n type: raw.type,\n quantity: raw.quantity,\n price: raw.price,\n tag: raw.tag,\n timeInForce: raw.timeInForce ?? 'gtc',\n status: 'pending',\n placedAt,\n });\n return id;\n }\n\n private closePosition(placedAt: number, tag?: string): string | null {\n const pos = this.portfolio.getPosition();\n if (!pos) return null;\n const closeSide: Side = pos.side === 'long' ? 'short' : 'long';\n return this.placeOrder(\n { side: closeSide, type: 'market', quantity: pos.quantity, tag },\n placedAt,\n );\n }\n\n private cancelOrder(orderId: string): boolean {\n const order = this.pendingOrders.find((o) => o.id === orderId);\n if (!order || order.status !== 'pending') return false;\n order.status = 'cancelled';\n return true;\n }\n}\n"],"names":["FixedCommission","perTrade","PercentCommission","rate","quantity","price","PerShareCommission","perShare","minimum","ZERO_COMMISSION","NO_SLIPPAGE","PercentSlippage","intendedPrice","side","adverse","RangeBasedSlippage","factor","bar","push","Portfolio","opts","dir","fill","signed","totalQty","closing","pnl","remaining","time","positionValue","unrealizedPnl","MS_PER_YEAR","computeRiskMetrics","initialCash","equityCurve","trades","emptyMetrics","finalEquity","totalReturn","totalReturnPct","periodsPerYear","inferPeriodsPerYear","periodRiskFree","returns","periodReturns","meanReturn","mean","stdDev","standardDeviation","downsideDev","downsideDeviation","sharpe","sortino","elapsedMs","years","cagr","maxDrawdown","maxDrawdownPct","computeDrawdown","calmar","tradeStats","summarizeTrades","curve","result","prev","values","sum","v","avg","acc","target","count","diff","peak","maxDd","maxDdPct","point","dd","wins","losses","totalWin","totalLoss","t","winRate","averageWin","averageLoss","profitFactor","expectancy","deltas","i","avgMs","perYear","Backtester","data","strategy","ctx","order","fillPrice","adjustedPrice","commission","o","index","history","portfolio","tag","orderId","raw","placedAt","pos","id","closeSide"],"mappings":"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"} |
+2
-2
| { | ||
| "name": "@tradecanvas/analytics", | ||
| "version": "0.8.1", | ||
| "version": "0.8.2", | ||
| "type": "module", | ||
@@ -43,3 +43,3 @@ "description": "Backtesting, portfolio tracking, and risk analytics for TradeCanvas — bar-by-bar Backtester with virtual fills, commission/slippage models, and risk metrics (Sharpe, Sortino, Calmar, max drawdown).", | ||
| "dependencies": { | ||
| "@tradecanvas/commons": "0.8.1" | ||
| "@tradecanvas/commons": "0.8.2" | ||
| }, | ||
@@ -46,0 +46,0 @@ "devDependencies": { |
101437
1.14%658
0.61%+ Added
- Removed
Updated