first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,251 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const statslite = require('stats-lite');
|
||||
const util = require('../../core/util');
|
||||
const log = require(util.dirs().core + 'log')
|
||||
const ENV = util.gekkoEnv();
|
||||
|
||||
const config = util.getConfig();
|
||||
const perfConfig = config.performanceAnalyzer;
|
||||
const watchConfig = config.watch;
|
||||
|
||||
const Logger = require('./logger');
|
||||
|
||||
const PerformanceAnalyzer = function() {
|
||||
_.bindAll(this);
|
||||
|
||||
this.dates = {
|
||||
start: false,
|
||||
end: false
|
||||
}
|
||||
|
||||
this.startPrice = 0;
|
||||
this.endPrice = 0;
|
||||
|
||||
this.currency = watchConfig.currency;
|
||||
this.asset = watchConfig.asset;
|
||||
|
||||
this.logger = new Logger(watchConfig);
|
||||
|
||||
this.trades = 0;
|
||||
|
||||
this.exposure = 0;
|
||||
|
||||
this.roundTrips = [];
|
||||
this.losses = [];
|
||||
this.roundTrip = {
|
||||
id: 0,
|
||||
entry: false,
|
||||
exit: false
|
||||
}
|
||||
|
||||
this.portfolio = {};
|
||||
this.balance;
|
||||
|
||||
this.start = {};
|
||||
this.openRoundTrip = false;
|
||||
|
||||
this.warmupCompleted = false;
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processPortfolioValueChange = function(event) {
|
||||
if(!this.start.balance) {
|
||||
this.start.balance = event.balance;
|
||||
}
|
||||
|
||||
this.balance = event.balance;
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processPortfolioChange = function(event) {
|
||||
if(!this.start.portfolio) {
|
||||
this.start.portfolio = event;
|
||||
}
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processStratWarmupCompleted = function() {
|
||||
this.warmupCompleted = true;
|
||||
this.processCandle(this.warmupCandle, _.noop);
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processCandle = function(candle, done) {
|
||||
if(!this.warmupCompleted) {
|
||||
this.warmupCandle = candle;
|
||||
return done();
|
||||
}
|
||||
|
||||
this.price = candle.close;
|
||||
this.dates.end = candle.start.clone().add(1, 'minute');
|
||||
|
||||
if(!this.dates.start) {
|
||||
this.dates.start = candle.start;
|
||||
this.startPrice = candle.close;
|
||||
}
|
||||
|
||||
this.endPrice = candle.close;
|
||||
|
||||
if(this.openRoundTrip) {
|
||||
this.emitRoundtripUpdate();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.emitRoundtripUpdate = function() {
|
||||
const uPnl = this.price - this.roundTrip.entry.price;
|
||||
|
||||
this.deferredEmit('roundtripUpdate', {
|
||||
at: this.dates.end,
|
||||
duration: this.dates.end.diff(this.roundTrip.entry.date),
|
||||
uPnl,
|
||||
uProfit: uPnl / this.roundTrip.entry.total * 100
|
||||
})
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processTradeCompleted = function(trade) {
|
||||
this.trades++;
|
||||
this.portfolio = trade.portfolio;
|
||||
this.balance = trade.balance;
|
||||
|
||||
this.registerRoundtripPart(trade);
|
||||
|
||||
const report = this.calculateReportStatistics();
|
||||
if(report) {
|
||||
this.logger.handleTrade(trade, report);
|
||||
this.deferredEmit('performanceReport', report);
|
||||
}
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.registerRoundtripPart = function(trade) {
|
||||
if(this.trades === 1 && trade.action === 'sell') {
|
||||
// this is not part of a valid roundtrip
|
||||
return;
|
||||
}
|
||||
|
||||
if(trade.action === 'buy') {
|
||||
if (this.roundTrip.exit) {
|
||||
this.roundTrip.id++;
|
||||
this.roundTrip.exit = false
|
||||
}
|
||||
|
||||
this.roundTrip.entry = {
|
||||
date: trade.date,
|
||||
price: trade.price,
|
||||
total: trade.portfolio.currency + (trade.portfolio.asset * trade.price),
|
||||
}
|
||||
this.openRoundTrip = true;
|
||||
} else if(trade.action === 'sell') {
|
||||
this.roundTrip.exit = {
|
||||
date: trade.date,
|
||||
price: trade.price,
|
||||
total: trade.portfolio.currency + (trade.portfolio.asset * trade.price),
|
||||
}
|
||||
this.openRoundTrip = false;
|
||||
|
||||
this.handleCompletedRoundtrip();
|
||||
}
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.handleCompletedRoundtrip = function() {
|
||||
var roundtrip = {
|
||||
id: this.roundTrip.id,
|
||||
|
||||
entryAt: this.roundTrip.entry.date,
|
||||
entryPrice: this.roundTrip.entry.price,
|
||||
entryBalance: this.roundTrip.entry.total,
|
||||
|
||||
exitAt: this.roundTrip.exit.date,
|
||||
exitPrice: this.roundTrip.exit.price,
|
||||
exitBalance: this.roundTrip.exit.total,
|
||||
|
||||
duration: this.roundTrip.exit.date.diff(this.roundTrip.entry.date)
|
||||
}
|
||||
|
||||
roundtrip.pnl = roundtrip.exitBalance - roundtrip.entryBalance;
|
||||
roundtrip.profit = (100 * roundtrip.exitBalance / roundtrip.entryBalance) - 100;
|
||||
|
||||
this.roundTrips[this.roundTrip.id] = roundtrip;
|
||||
|
||||
this.logger.handleRoundtrip(roundtrip);
|
||||
|
||||
this.deferredEmit('roundtrip', roundtrip);
|
||||
|
||||
// update cached exposure
|
||||
this.exposure = this.exposure + Date.parse(this.roundTrip.exit.date) - Date.parse(this.roundTrip.entry.date);
|
||||
// track losses separately for downside report
|
||||
if (roundtrip.exitBalance < roundtrip.entryBalance)
|
||||
this.losses.push(roundtrip);
|
||||
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.calculateReportStatistics = function() {
|
||||
if(!this.start.balance || !this.start.portfolio) {
|
||||
log.error('Cannot calculate a profit report without having received portfolio data.');
|
||||
log.error('Skipping performanceReport..');
|
||||
return false;
|
||||
}
|
||||
|
||||
// the portfolio's balance is measured in {currency}
|
||||
const profit = this.balance - this.start.balance;
|
||||
|
||||
const timespan = moment.duration(
|
||||
this.dates.end.diff(this.dates.start)
|
||||
);
|
||||
const relativeProfit = this.balance / this.start.balance * 100 - 100;
|
||||
const relativeYearlyProfit = relativeProfit / timespan.asYears();
|
||||
|
||||
const percentExposure = this.exposure / (Date.parse(this.dates.end) - Date.parse(this.dates.start));
|
||||
|
||||
const sharpe = (relativeYearlyProfit - perfConfig.riskFreeReturn)
|
||||
/ statslite.stdev(this.roundTrips.map(r => r.profit))
|
||||
/ Math.sqrt(this.trades / (this.trades - 2));
|
||||
|
||||
const downside = statslite.percentile(this.losses.map(r => r.profit), 0.25)
|
||||
* Math.sqrt(this.trades / (this.trades - 2));
|
||||
|
||||
const ratioRoundTrips = this.roundTrips.length > 0 ? (this.roundTrips.filter(roundTrip => roundTrip.pnl > 0 ).length / this.roundTrips.length * 100).toFixed(4) : 100;
|
||||
|
||||
const report = {
|
||||
startTime: this.dates.start.utc().format('YYYY-MM-DD HH:mm:ss'),
|
||||
endTime: this.dates.end.utc().format('YYYY-MM-DD HH:mm:ss'),
|
||||
timespan: timespan.humanize(),
|
||||
market: this.endPrice * 100 / this.startPrice - 100,
|
||||
|
||||
balance: this.balance,
|
||||
profit,
|
||||
relativeProfit: relativeProfit,
|
||||
|
||||
yearlyProfit: profit / timespan.asYears(),
|
||||
relativeYearlyProfit,
|
||||
|
||||
startPrice: this.startPrice,
|
||||
endPrice: this.endPrice,
|
||||
trades: this.trades,
|
||||
startBalance: this.start.balance,
|
||||
exposure: percentExposure,
|
||||
sharpe,
|
||||
downside,
|
||||
ratioRoundTrips
|
||||
}
|
||||
|
||||
report.alpha = report.relativeProfit - report.market;
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.finalize = function(done) {
|
||||
if(!this.trades) {
|
||||
return done();
|
||||
}
|
||||
|
||||
const report = this.calculateReportStatistics();
|
||||
if(report) {
|
||||
this.logger.finalize(report);
|
||||
this.emit('performanceReport', report);
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
|
||||
module.exports = PerformanceAnalyzer;
|
||||
Reference in new issue
Block a user