first commit

This commit is contained in:
Francois Klieber committed 2021-06-06 20:25:14 +02:00
commit aa7869eb33
386 files changed
+69375

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
// log trade performance results
const _ = require('lodash');
const moment = require('moment');
const humanizeDuration = require('humanize-duration');
const util = require('../../core/util.js');
const dirs = util.dirs();
const mode = util.gekkoMode();
const log = require(dirs.core + 'log');
const Logger = function(watchConfig) {
this.currency = watchConfig.currency;
this.asset = watchConfig.asset;
this.roundtrips = [];
}
Logger.prototype.round = function(amount) {
return amount.toFixed(8);
}
// used for:
// - realtime logging (per advice)
// - backtest logging (on finalize)
Logger.prototype.logReport = function(trade, report) {
// ignore the trade
var start = this.round(report.startBalance);
var current = this.round(report.balance);
log.info(`(PROFIT REPORT) original balance:\t\t ${start} ${this.currency}`);
log.info(`(PROFIT REPORT) current balance:\t\t ${current} ${this.currency}`);
log.info(
`(PROFIT REPORT) profit:\t\t\t\t ${this.round(report.profit)} ${this.currency}`,
`(${this.round(report.relativeProfit)}%)`
);
}
Logger.prototype.logRoundtripHeading = function() {
log.info('(ROUNDTRIP)', 'entry date (UTC) \texit date (UTC) \texposed duration\tP&L \tprofit');
}
Logger.prototype.logRoundtrip = function(rt) {
const display = [
rt.entryAt.utc().format('YYYY-MM-DD HH:mm'),
rt.exitAt.utc().format('YYYY-MM-DD HH:mm'),
(moment.duration(rt.duration).humanize() + " ").slice(0, 16),
rt.pnl.toFixed(2),
rt.profit.toFixed(2)
];
log.info('(ROUNDTRIP)', display.join('\t'));
}
if(mode === 'backtest') {
// we only want to log a summarized one line report, like:
// 2016-12-19 20:12:00: Paper trader simulated a BUY 0.000 USDT => 1.098 BTC
Logger.prototype.handleTrade = function(trade) {
if(trade.action !== 'sell' && trade.action !== 'buy')
return;
var at = trade.date.format('YYYY-MM-DD HH:mm:ss');
if(trade.action === 'sell')
log.info(
`${at}: Paper trader simulated a SELL`,
`\t${this.round(trade.portfolio.currency)}`,
`${this.currency} <= ${this.round(trade.portfolio.asset)}`,
`${this.asset}`
);
else if(trade.action === 'buy')
log.info(
`${at}: Paper trader simulated a BUY`,
`\t${this.round(trade.portfolio.currency)}`,
`${this.currency}\t=> ${this.round(trade.portfolio.asset)}`,
`${this.asset}`
);
}
Logger.prototype.finalize = function(report) {
log.info();
log.info('(ROUNDTRIP) REPORT:');
this.logRoundtripHeading();
_.each(this.roundtrips, this.logRoundtrip, this);
log.info()
log.info(`(PROFIT REPORT) start time:\t\t\t ${report.startTime}`);
log.info(`(PROFIT REPORT) end time:\t\t\t ${report.endTime}`);
log.info(`(PROFIT REPORT) timespan:\t\t\t ${report.timespan}`);
log.info(`(PROFIT REPORT) exposure:\t\t\t ${report.exposure}`);
log.info();
log.info(`(PROFIT REPORT) start price:\t\t\t ${report.startPrice} ${this.currency}`);
log.info(`(PROFIT REPORT) end price:\t\t\t ${report.endPrice} ${this.currency}`);
log.info(`(PROFIT REPORT) Market:\t\t\t\t ${this.round(report.market)}%`);
log.info();
log.info(`(PROFIT REPORT) amount of trades:\t\t ${report.trades}`);
this.logReport(null, report);
log.info(
`(PROFIT REPORT) simulated yearly profit:\t ${report.yearlyProfit}`,
`${this.currency} (${report.relativeYearlyProfit}%)`
);
log.info(`(PROFIT REPORT) sharpe ratio:\t\t\t ${report.sharpe}`);
log.info(`(PROFIT REPORT) expected downside:\t\t ${report.downside}`);
log.info(`(PROFIT REPORT) ratio roundtrips:\t\t ${report.ratioRoundTrips}%`);
}
Logger.prototype.handleRoundtrip = function(rt) {
this.roundtrips.push(rt);
}
} else if(mode === 'realtime') {
Logger.prototype.handleTrade = Logger.prototype.logReport;
Logger.prototype.handleRoundtrip = function(rt) {
this.logRoundtripHeading();
this.logRoundtrip(rt);
}
}
module.exports = Logger;
@@ -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;