net.tradelib.apps.StrategyBacktest.java Source code

Java tutorial

Introduction

Here is the source code for net.tradelib.apps.StrategyBacktest.java

Source

// Copyright 2015 Ivan Popivanov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package net.tradelib.apps;

import java.io.File;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage.RecipientType;

import org.apache.commons.io.FileUtils;

import net.tradelib.core.Average;
import net.tradelib.core.Context;
import net.tradelib.core.HistoricalDataFeed;
import net.tradelib.core.HistoricalReplay;
import net.tradelib.core.SQLDataFeed;
import net.tradelib.core.Series;
import net.tradelib.core.Strategy;
import net.tradelib.core.TimeSeries;
import net.tradelib.core.TradeSummary;
import net.tradelib.misc.SftpUploader;
import net.tradelib.misc.StrategyText;

import com.google.common.base.Strings;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

public class StrategyBacktest {

    public static void run(Strategy strategy) throws Exception {
        // Setup the logging
        System.setProperty("java.util.logging.SimpleFormatter.format",
                "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS: %4$s: %5$s%n%6$s%n");
        LogManager.getLogManager().reset();
        Logger rootLogger = Logger.getLogger("");
        if (Boolean.parseBoolean(BacktestCfg.instance().getProperty("file.log", "true"))) {
            FileHandler logHandler = new FileHandler("diag.out", 8 * 1024 * 1024, 2, true);
            logHandler.setFormatter(new SimpleFormatter());
            logHandler.setLevel(Level.FINEST);
            rootLogger.addHandler(logHandler);
        }

        if (Boolean.parseBoolean(BacktestCfg.instance().getProperty("console.log", "true"))) {
            ConsoleHandler consoleHandler = new ConsoleHandler();
            consoleHandler.setFormatter(new SimpleFormatter());
            consoleHandler.setLevel(Level.INFO);
            rootLogger.addHandler(consoleHandler);
        }

        rootLogger.setLevel(Level.INFO);

        // Setup Hibernate
        // Configuration configuration = new Configuration();
        // StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties());
        // SessionFactory factory = configuration.buildSessionFactory(builder.build());

        Context context = new Context();
        context.dbUrl = BacktestCfg.instance().getProperty("db.url");

        HistoricalDataFeed hdf = new SQLDataFeed(context);
        hdf.configure(BacktestCfg.instance().getProperty("datafeed.config", "config/datafeed.properties"));
        context.historicalDataFeed = hdf;

        HistoricalReplay hr = new HistoricalReplay(context);
        context.broker = hr;

        strategy.initialize(context);
        strategy.cleanupDb();

        long start = System.nanoTime();
        strategy.start();
        long elapsedTime = System.nanoTime() - start;
        System.out.println("backtest took " + String.format("%.2f secs", (double) elapsedTime / 1e9));

        start = System.nanoTime();
        strategy.updateEndEquity();
        strategy.writeExecutionsAndTrades();
        strategy.writeEquity();
        elapsedTime = System.nanoTime() - start;
        System.out
                .println("writing to the database took " + String.format("%.2f secs", (double) elapsedTime / 1e9));

        System.out.println();

        // Write the strategy totals to the database
        strategy.totalTradeStats();

        // Write the strategy report to the database and obtain the JSON
        // for writing it to the console.
        JsonObject report = strategy.writeStrategyReport();

        JsonArray asa = report.getAsJsonArray("annual_stats");

        String csvPath = BacktestCfg.instance().getProperty("positions.csv.prefix");
        if (!Strings.isNullOrEmpty(csvPath)) {
            csvPath += "-" + strategy.getLastTimestamp().toLocalDate().format(DateTimeFormatter.BASIC_ISO_DATE)
                    + ".csv";
        }

        String ordersCsvPath = BacktestCfg.instance().getProperty("orders.csv.suffix");
        if (!Strings.isNullOrEmpty(ordersCsvPath)) {
            ordersCsvPath = strategy.getLastTimestamp().toLocalDate().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"
                    + strategy.getName() + ordersCsvPath;
        }

        String actionsPath = BacktestCfg.instance().getProperty("actions.file.suffix");
        if (!Strings.isNullOrEmpty(actionsPath)) {
            actionsPath = strategy.getLastTimestamp().toLocalDate().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"
                    + strategy.getName() + actionsPath;
        }

        // If emails are being send out
        String signalText = StrategyText.build(context.dbUrl, strategy.getName(),
                strategy.getLastTimestamp().toLocalDate(), "   ", csvPath, '|');

        System.out.println(signalText);
        System.out.println();

        if (!Strings.isNullOrEmpty(ordersCsvPath)) {
            StrategyText.buildOrdersCsv(context.dbUrl, strategy.getName(),
                    strategy.getLastTimestamp().toLocalDate(), ordersCsvPath);
        }

        File actionsFile = Strings.isNullOrEmpty(actionsPath) ? null : new File(actionsPath);

        if (actionsFile != null) {
            FileUtils.writeStringToFile(actionsFile,
                    signalText + System.getProperty("line.separator") + System.getProperty("line.separator"));
        }

        String message = "";

        if (asa.size() > 0) {
            // Sort the array
            TreeMap<Integer, Integer> map = new TreeMap<Integer, Integer>();
            for (int ii = 0; ii < asa.size(); ++ii) {
                int year = asa.get(ii).getAsJsonObject().get("year").getAsInt();
                map.put(year, ii);
            }

            for (int id : map.values()) {
                JsonObject jo = asa.get(id).getAsJsonObject();
                String yearStr = String.valueOf(jo.get("year").getAsInt());
                String pnlStr = String.format("$%,d", jo.get("pnl").getAsInt());
                String pnlPctStr = String.format("%.2f%%", jo.get("pnl_pct").getAsDouble());
                String endEqStr = String.format("$%,d", jo.get("end_equity").getAsInt());
                String ddStr = String.format("$%,d", jo.get("maxdd").getAsInt());
                String ddPctStr = String.format("%.2f%%", jo.get("maxdd_pct").getAsDouble());
                String str = yearStr + " PnL: " + pnlStr + ", PnL Pct: " + pnlPctStr + ", End Equity: " + endEqStr
                        + ", MaxDD: " + ddStr + ", Pct MaxDD: " + ddPctStr;
                message += str + "\n";
            }

            String pnlStr = String.format("$%,d", report.get("pnl").getAsInt());
            String pnlPctStr = String.format("%.2f%%", report.get("pnl_pct").getAsDouble());
            String ddStr = String.format("$%,d", report.get("avgdd").getAsInt());
            String ddPctStr = String.format("%.2f%%", report.get("avgdd_pct").getAsDouble());
            String gainToPainStr = String.format("%.4f", report.get("gain_to_pain").getAsDouble());
            String str = "\nAvg PnL: " + pnlStr + ", Pct Avg PnL: " + pnlPctStr + ", Avg DD: " + ddStr
                    + ", Pct Avg DD: " + ddPctStr + ", Gain to Pain: " + gainToPainStr;
            message += str + "\n";
        } else {
            message += "\n";
        }

        // Global statistics
        JsonObject jo = report.getAsJsonObject("total_peak");
        String dateStr = jo.get("date").getAsString();
        int maxEndEq = jo.get("equity").getAsInt();
        jo = report.getAsJsonObject("total_maxdd");
        double cash = jo.get("cash").getAsDouble();
        double pct = jo.get("pct").getAsDouble();
        message += "\n" + "Total equity peak [" + dateStr + "]: " + String.format("$%,d", maxEndEq) + "\n"
                + String.format("Current Drawdown: $%,d [%.2f%%]", Math.round(cash), pct) + "\n";

        if (report.has("latest_peak") && report.has("latest_maxdd")) {
            jo = report.getAsJsonObject("latest_peak");
            LocalDate ld = LocalDate.parse(jo.get("date").getAsString(), DateTimeFormatter.ISO_DATE);
            maxEndEq = jo.get("equity").getAsInt();
            jo = report.getAsJsonObject("latest_maxdd");
            cash = jo.get("cash").getAsDouble();
            pct = jo.get("pct").getAsDouble();
            message += "\n" + Integer.toString(ld.getYear()) + " equity peak ["
                    + ld.format(DateTimeFormatter.ISO_DATE) + "]: " + String.format("$%,d", maxEndEq) + "\n"
                    + String.format("Current Drawdown: $%,d [%.2f%%]", Math.round(cash), pct) + "\n";
        }

        message += "\n" + "Avg Trade PnL: "
                + String.format("$%,d", Math.round(report.get("avg_trade_pnl").getAsDouble())) + ", Max DD: "
                + String.format("$%,d", Math.round(report.get("maxdd").getAsDouble())) + ", Max DD Pct: "
                + String.format("%.2f%%", report.get("maxdd_pct").getAsDouble()) + ", Num Trades: "
                + Integer.toString(report.get("num_trades").getAsInt());

        System.out.println(message);

        if (actionsFile != null) {
            FileUtils.writeStringToFile(actionsFile, message + System.getProperty("line.separator"), true);
        }

        if (Boolean.parseBoolean(BacktestCfg.instance().getProperty("email.enabled", "false"))) {
            Properties props = new Properties();
            props.put("mail.smtp.auth", "true");
            props.put("mail.smtp.starttls.enable", "true");
            props.put("mail.smtp.host", "smtp.sendgrid.net");
            props.put("mail.smtp.port", "587");

            String user = BacktestCfg.instance().getProperty("email.user");
            String pass = BacktestCfg.instance().getProperty("email.pass");

            Session session = Session.getInstance(props, new javax.mail.Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(user, pass);
                }
            });

            MimeMessage msg = new MimeMessage(session);
            try {
                msg.setFrom(new InternetAddress(BacktestCfg.instance().getProperty("email.from")));
                msg.addRecipients(RecipientType.TO, BacktestCfg.instance().getProperty("email.recipients"));
                msg.setSubject(strategy.getName() + " Report ["
                        + strategy.getLastTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE) + "]");
                msg.setText("Positions & Signals\n" + signalText + "\n\nStatistics\n" + message);
                Transport.send(msg);
            } catch (Exception ee) {
                Logger.getLogger("").warning(ee.getMessage());
            }
        }

        if (Boolean.parseBoolean(BacktestCfg.instance().getProperty("sftp.enabled", "false"))) {
            HashMap<String, String> fileMap = new HashMap<String, String>();
            if (!Strings.isNullOrEmpty(actionsPath))
                fileMap.put(actionsPath, actionsPath);
            if (!Strings.isNullOrEmpty(ordersCsvPath))
                fileMap.put(ordersCsvPath, ordersCsvPath);
            String user = BacktestCfg.instance().getProperty("sftp.user");
            String pass = BacktestCfg.instance().getProperty("sftp.pass");
            String host = BacktestCfg.instance().getProperty("sftp.host");
            SftpUploader sftp = new SftpUploader(host, user, pass);
            sftp.upload(fileMap);
        }
    }

    private class PerformanceDetails {
        public LocalDateTime maxEquityDateTime;
        public double maxEquity;
        public double lastDrawdown;

        public void build(TimeSeries<Double> pnl) {
            maxEquityDateTime = LocalDateTime.MIN;
            maxEquity = Double.MIN_VALUE;

            double equity = 0.0;

            for (int ii = 0; ii < pnl.size(); ++ii) {
                equity += pnl.get(ii);
                if (equity > maxEquity) {
                    maxEquity = equity;
                    maxEquityDateTime = pnl.getTimestamp(ii);
                }
            }

            lastDrawdown = maxEquity - equity;
        }
    }
}