package org.jinstagram.http;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.jinstagram.auth.exceptions.OAuthException;

/**
 * Represents an HTTP Request object
 */
public class Request {
	private static final String CONTENT_LENGTH = "Content-Length";

	private String payload = null;

	private boolean connectionKeepAlive = false;

	private Map<String, String> bodyParams;

	private String charset;

	private HttpURLConnection connection;

	private Map<String, String> headers;

	private Map<String, String> querystringParams;

	private String url;

	private Verbs verb;

	private int connectTimeout;

	private int readTimeout;

	private Proxy proxy;

	/**
	 * Creates a new Http Request
	 *
	 * @param verb Http Verb (GET, POST, etc)
	 * @param url url with optional querystring parameters.
	 */
	public Request(Verbs verb, String url) {
		this.verb = verb;
		this.url = url;
		this.querystringParams = new HashMap<String, String>();
		this.bodyParams = new HashMap<String, String>();
		this.headers = new HashMap<String, String>();
	}

	/**
	 * Execute the request and return a {@link Response}
	 *
	 * @return Http Response
	 * @throws RuntimeException if the connection cannot be created.
	 */
	public Response send() throws IOException {
		createConnection();

		return doSend();
	}

	private void createConnection() throws IOException {
		String effectiveUrl = URLUtils.appendParametersToQueryString(url, querystringParams);

		if (connection == null) {
			System.setProperty("http.keepAlive", connectionKeepAlive ? "true" : "false");

			URL url = new URL(effectiveUrl);
			connection = (HttpURLConnection) (proxy == null ?
					url.openConnection() : url.openConnection(proxy));

			connection.setConnectTimeout(connectTimeout);
			connection.setReadTimeout(readTimeout);
		}
	}

	Response doSend() throws IOException {
		connection.setRequestMethod(this.verb.name());
		addHeaders(connection);

		if (verb.equals(Verbs.PUT) || verb.equals(Verbs.POST)) {
			addBody(connection, getByteBodyContents());
		}

		return new Response(connection);
	}

	void addHeaders(HttpURLConnection conn) {
		for (Map.Entry<String, String> entry : headers.entrySet()) {
			conn.setRequestProperty(entry.getKey(), entry.getValue());
		}
	}

	void addBody(HttpURLConnection conn, byte[] content) throws IOException {
		conn.setRequestProperty(CONTENT_LENGTH, String.valueOf(content.length));
		conn.setDoOutput(true);
		conn.getOutputStream().write(content);
	}

	/**
	 * Add an HTTP Header to the Request
	 *
	 * @param key the header name
	 * @param value the header value
	 */
	public void addHeader(String key, String value) {
		this.headers.put(key, value);
	}

	/**
	 * Add a body Parameter (for POST/ PUT Requests)
	 *
	 * @param key the parameter name
	 * @param value the parameter value
	 */
	public void addBodyParameter(String key, String value) {
		this.bodyParams.put(key, value);
	}

	/**
	 * Add a QueryString parameter
	 *
	 * @param key the parameter name
	 * @param value the parameter value
	 */
	public void addQuerystringParameter(String key, String value) {
		this.querystringParams.put(key, value);
	}

	/**
	 * Add body payload.
	 *
	 * This method is used when the HTTP body is not a form-url-encoded string,
	 * but another thing. Like for example XML.
	 *
	 * Note: The contents are not part of the OAuth signature
	 *
	 * @param payload the body of the request
	 */
	public void addPayload(String payload) {
		this.payload = payload;
	}

	/**
	 * Get a {@link Map} of the query string parameters.
	 *
	 * @return a map containing the query string parameters
	 * @throws OAuthException if the URL is not valid
	 */
	public Map<String, String> getQueryStringParams() {
		try {
			Map<String, String> params = new HashMap<String, String>();
			String queryString = new URL(url).getQuery();

			params.putAll(URLUtils.queryStringToMap(queryString));
			params.putAll(this.querystringParams);

			return params;
		} catch (MalformedURLException mue) {
			throw new OAuthException("Malformed URL", mue);
		}
	}

	/**
	 * Obtains a {@link Map} of the body parameters.
	 *
	 * @return a map containing the body parameters.
	 */
	public Map<String, String> getBodyParams() {
		return bodyParams;
	}

	/**
	 * Obtains the URL of the HTTP Request.
	 *
	 * @return the original URL of the HTTP Request
	 */
	public String getUrl() {
		return url;
	}

	/**
	 * Returns the URL without the port and the query string part.
	 *
	 * @return the OAuth-sanitized URL
	 */
	public String getSanitizedUrl() {
		return url.replaceAll("\\?.*", "").replace("\\:\\d{4}", "");
	}

	/**
	 * Returns the body of the request
	 *
	 * @return form encoded string
	 * @throws OAuthException if the charset chosen is not supported
	 */
	public String getBodyContents() {
		try {
			return new String(getByteBodyContents(), getCharset());
		} catch (UnsupportedEncodingException uee) {
			throw new OAuthException("Unsupported Charset: " + charset, uee);
		}
	}

	byte[] getByteBodyContents() {

		String body = (payload != null) ? payload : URLUtils.formURLEncodeMap(bodyParams);

		try {
			return body.getBytes(getCharset());
		} catch (UnsupportedEncodingException uee) {
			throw new OAuthException("Unsupported Charset: " + getCharset(), uee);
		}
	}

	/**
	 * Returns the HTTP Verb
	 *
	 * @return the verb
	 */
	public Verbs getVerb() {
		return verb;
	}

	/**
	 * Returns the connection headers as a {@link Map}
	 *
	 * @return map of headers
	 */
	public Map<String, String> getHeaders() {
		return headers;
	}

	/**
	 * Returns the connection charset. Defaults to {@link Charset}
	 * defaultCharset if not set
	 *
	 * @return charset
	 */
	public String getCharset() {
		return (charset == null) ? Charset.defaultCharset().name() : charset;
	}

	/**
	 * Sets the connect timeout for the underlying {@link HttpURLConnection}
	 *
	 * @param duration duration of the timeout
	 *
	 * @param unit unit of time (milliseconds, seconds, etc)
	 */
	public void setConnectTimeout(int duration, TimeUnit unit) {
		this.connectTimeout = (int) unit.toMillis(duration);
	}

	/**
	 * Gets the connect timeout in millisecond units
	 */
	public int getConnectTimeoutInMillis() {
	    return connectTimeout;
	}

	/**
	 * Sets the read timeout for the underlying {@link HttpURLConnection}
	 *
	 * @param duration duration of the timeout
	 *
	 * @param unit unit of time (milliseconds, seconds, etc)
	 */
	public void setReadTimeout(int duration, TimeUnit unit) {
		this.readTimeout = (int) unit.toMillis(duration);
	}

	/**
	 * Gets the read timeout in millisecond units
	 */
	public int getReadTimeoutInMillis() {
	    return readTimeout;
	}

	/**
	 * Set the charset of the body of the request
	 *
	 * @param charsetName name of the charset of the request
	 */
	public void setCharset(String charsetName) {
		this.charset = charsetName;
	}

	/**
	 * Sets wether the underlying Http Connection is persistent or not.
	 *
	 * @see http://download.oracle.com/javase/1.5.0/docs/guide/net/http-keepalive.html
	 * @param connectionKeepAlive
	 */
	public void setConnectionKeepAlive(boolean connectionKeepAlive) {
		this.connectionKeepAlive = connectionKeepAlive;
	}

	/**
	 * Returns the connection keepalive setting
	 */
	public boolean getConnectionKeepAlive() {
	    return connectionKeepAlive;
	}

	/*
	 * We need this in order to stub the connection object for test cases
	 */
	void setConnection(HttpURLConnection connection) {
		this.connection = connection;
	}

	/**
	 * Set the proxy that will be used for the rquest
	 * @param proxy proxy to be used
	 */
	public void setProxy(Proxy proxy) {
		this.proxy = proxy;
	}

	/**
	 * Return the request proxy
	 */
	public Proxy getProxy() {
	    return proxy;
	}

	@Override
	public String toString() {
		return String.format("@Request(%s %s)", getVerb(), getUrl());
	}
}


package org.jinstagram.utils;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.slf4j.Logger;

public final class LogHelper {

    /**
     *

     * The message format pattern used to log the exception.
     *

     */
    private static final String EXCEPTION_PATTERN = "Error in method {0}.\n\tException name: {1}.\n\t"
            + "Stack trace: {2}.\n\tTime spent in the method: {3} milliseconds.";

    /**
     *

     * The message format pattern used to log the exit of method.
     *

     */
    private static final String EXIT_METHOD_PATTERN = "Exiting method %s.\n\tReturn value: [{%s}].\n\tTime spent in the method: {2" +
            "%d} milliseconds.";

    /**
     *

     * The message format pattern used to log the entrance of method.
     *

     */
    private static final String ENTRANCE_METHOD_PATTERN = "Entering method %s.\n\tMethod arguments: [%s].";

    private static Gson GSON = new GsonBuilder().setPrettyPrinting().create();

    /**
     *

     * Private default constructor, so the LogHelper can not be created
     * by the outside code.
     *

     */
    private LogHelper() {
    }

    /**
     *

     * Logs for entrance into every public methods with {@link Level#DEBUG}.
     *

     *
     * @param methodName      The name of method entranced into.
     * @param methodArguments The method arguments
     * @param logger          the logger instance
     */
    public static void logEntrance(Logger logger, String methodName, String methodArguments) {
        logger.debug(String.format(ENTRANCE_METHOD_PATTERN, methodName, methodArguments));

    }

    /**
     *

     * Logs for exit from every public methods with {@link Level#DEBUG}.
     *

     *
     * @param methodName     The name of method existed from.
     * @param returnValue    the return value
     * @param timeSpent      the time spent
     * @param logger         the logger instance
     * @param verboseLogging the control flag defining whether the detailed logging actions should be performed
     */
    public static void logExit(Logger logger, String methodName, String returnValue,
                               long timeSpent) {
        logger.debug(String.format(EXIT_METHOD_PATTERN, methodName, returnValue, timeSpent));

    }

    /**
     *

     * Logs the given Throwable with {@link Level#ERROR}.
     *
;2
     *
     * @param methodName     The name of method where the exception occurs.
     * @param e              The Throwable caught to be logged.
     * @param timeSpent      the time spent
     * @param logger         the logger instance
     * @param verboseLogging the control flag defining whether the detailed logging actions should be performed
     */
    public static void logException(Logger logger, String methodName, Throwable e,
                                    long timeSpent) {
        logger.error(String.format(EXCEPTION_PATTERN, methodName, e.getClass().getSimpleName(), e.getMessage()));
    }

    /**
     * Pretty print the JSON response in the logs
     *
     * @param logger     the logger instance
     * @param jsonString the json String which is to be pretty printed.
     */
    public static void prettyPrintJSONResponse(Logger logger, String jsonString) {
        if(logger.isDebugEnabled()) {
            // it can fail...on 404 it usually not a json
            String s;
            try {
                final JsonElement element = new JsonParser().parse(jsonString);
                s = new GsonBuilder().setPrettyPrinting().create().toJson(element);
            } catch(Exception e) {
                s = jsonString;
            }

            logger.debug("Received JSON response from Instagram - " + s);
        }
    }

}


package org.jinstagram.realtime;

import static org.jinstagram.Instagram.configureConnectionSettings;

import org.jinstagram.InstagramConfig;
import org.jinstagram.auth.model.OAuthConstants;
import org.jinstagram.auth.model.OAuthRequest;
import org.jinstagram.exceptions.InstagramException;
import org.jinstagram.http.Response;
import org.jinstagram.http.Verbs;
import org.jinstagram.model.Methods;
import org.jinstagram.utils.Preconditions;

import com.google.gson.Gson;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Deprecated
public class InstagramSubscription {

	private final Map<String, String> params;
	private InstagramConfig config = new InstagramConfig();

	/**
	 * Default constructor
	 */
	public InstagramSubscription() {
		this.params = new HashMap<String, String>();
		this.params.put(Constants.CALLBACK_URL, OAuthConstants.OUT_OF_BAND);
	}

	/**
	 * Configures the callback url
	 *
	 * @param callback The callback for your application
	 * @return the {@link InstagramSubscription} instance for method chaining
	 */
	public InstagramSubscription callback(String callback) {
		Preconditions.checkValidUrl(callback, "Invalid Callback Url");

		this.params.put(Constants.CALLBACK_URL, callback);

		return this;
	}

	/**
	 * Configures the clientId
	 *
	 * @param clientId The clientId for your application
	 * @return the {@link InstagramSubscription} instance for method chaining
	 */
	public InstagramSubscription clientId(String clientId) {
		Preconditions.checkEmptyString(clientId, "Invalid 'clientId' key");

		this.params.put(Constants.CLIENT_ID, clientId);

		return this;
	}

	/**
	 * Configure the clientSecret
	 * @param clientSecret The clientSecret for your application
	 * @return the {@link InstagramSubscription} instance for method chaining
	 */
	public InstagramSubscription clientSecret(String clientSecret) {
		Preconditions.checkEmptyString(clientSecret, "Invalid 'clientSecret' key");

		this.params.put(Constants.CLIENT_SECRET, clientSecret);

		return this;
	}

	/**
	 * Configure the subscription type
	 * @param type The subscription type for your usage
	 * @return the {@link InstagramSubscription} instance for method chaining
	 */
	public InstagramSubscription object(SubscriptionType type) {

		this.params.put(Constants.SUBSCRIPTION_TYPE, type.toString());

		return this;
	}

    /**
     * Configures the target id for the subscription, if any. In case of a a real-time tag update, this
     * defines the target tag to use.
     *
     * @param objectId the object to target for this real-time subscription
     * @return the {@link InstagramSubscription} instance for method chaining
     */
    public InstagramSubscription objectId(String objectId) {
    	this.params.put(Constants.OBJECT_ID, objectId);
        return this;
    }

    /**
     * Configure the verifyToken for the subscription
     *
     * @param verifyToken The token for the subscription
     * @return the {@link InstagramSubscription} instance for method chaining
     */
	public InstagramSubscription verifyToken(String verifyToken) {
		Preconditions.checkEmptyString(verifyToken, "Invalid 'verifyToken' key");

		this.params.put(Constants.VERIFY_TOKEN, verifyToken);

		return this;
	}

	/**
	 * Configure the aspect for the subscription
	 *
	 * @param aspect The aspect of the subscription
	 * @return the {@link InstagramSubscription} instance for method chaining
	 */
	public InstagramSubscription aspect(String aspect) {
		Preconditions.checkEmptyString(aspect, "Invalid 'aspect' key");

		this.params.put(Constants.ASPECT, aspect);

		return this;
	}

	/**
	 * Configure the latitude for geography subscription
	 * @param latitude The latitude for geography subscription
	 * @return the {@link InstagramSubscription} instance for method chaining
	 */
	public InstagramSubscription latitute(String latitude){
		Preconditions.checkValidLatLong(latitude, "Invalid 'lat' key");

		this.params.put(Constants.LATITUDE, latitude);

		return this;
	}

	/**
	 * Configure the longitude for geography subscription
	 * @param longitude The longitude for geography subscription
	 * @return the {@link InstagramSubscription} instance for method chaining
	 */
	public InstagramSubscription longitude(String longitude){
		Preconditions.checkValidLatLong(longitude, "Invalid 'lng' key");

		this.params.put(Constants.LONGITUDE, longitude);

		return this;
	}

	/**
	 * Configure the radius for geography subscription
	 * @param radius The radius for geography subscription in metre
	 * @return the {@link InstagramSubscription} instance for method chaining
	 */
	public InstagramSubscription radius(String radius){
		Preconditions.checkValidRadius(radius, "Invalid 'radius' key");

		this.params.put(Constants.RADIUS, radius);

		return this;
	}

	/**
	 * Configure the connection configuration
	 */
	public InstagramSubscription requestConfiguration(final InstagramConfig config){
		if(config != null) {
			this.config = config;
		}
		return this;
	}

    /**
     * Creates a subscription with the current state of this instance.
     * <p/>
     * The {@link SubscriptionResponse} holds the result of the subscription such as the identifier
     * of the subscription that can be used later to {@linkplain #deleteSubscription(String) delete it}.
     *
     * @return the result of the subscription
     * @throws InstagramException
     */
	public SubscriptionResponse createSubscription() throws InstagramException {
		String callback = params.get(Constants.CALLBACK_URL);
		if(callback == null){
			callback = "";
		}
		Preconditions.checkEmptyString(callback, "You must provide a callback url");

        final OAuthRequest request = prepareOAuthRequest(Verbs.POST);
		request.addBodyParameter(Constants.ASPECT, "media");

		for(Map.Entry<String, String> entry: this.params.entrySet()){
			request.addBodyParameter(entry.getKey(), entry.getValue());
		}

        try {
            final Response response = request.send();
            return getSubscriptionResponse(response.getBody());
        } catch (IOException e) {
            throw new InstagramException("Failed to create subscription", e);
        }
	}

    /**
     * Deletes a subscription with the specified identifier.
     *
     * @param id the id of the subscription to remove
     */
    public SubscriptionResponse deleteSubscription(String id) throws InstagramException {
        final OAuthRequest request = prepareOAuthRequest(Verbs.DELETE);
        request.addQuerystringParameter("id", id);

        try {
            final Response response = request.send();
            return getSubscriptionResponse(response.getBody());
        } catch (IOException e) {
            throw new InstagramException("Failed to delete subscription with id ["+id+"]", e);
        }
    }

    /**
     * Deletes all the known subscription.
     *
     * @return the response of this request, holding mainly the code
     */
	public SubscriptionResponse deleteAllSubscription() throws InstagramException {
        final OAuthRequest request = prepareOAuthRequest(Verbs.DELETE);
		request.addQuerystringParameter(Constants.SUBSCRIPTION_TYPE, "all");

        try {
            final Response response = request.send();
            return getSubscriptionResponse(response.getBody());
        } catch (IOException e) {
            throw new InstagramException("Failed to delete all subscriptions", e);
        }
	}

    /**
     * Returns the currently active subscription.
     *
     * @return the active subscription
     */
	public SubscriptionsListResponse getSubscriptionList() throws InstagramException {
		final OAuthRequest request = prepareOAuthRequest(Verbs.GET);

        try {
            final Response response = request.send();
            return getSubscriptionsListResponse(response.getBody());
        } catch (IOException e) {
            throw new InstagramException("Failed to get subscription list", e);
        }
	}

    private OAuthRequest prepareOAuthRequest(Verbs verb) {
    	String clientId = params.get(Constants.CLIENT_ID);
        Preconditions.checkEmptyString(clientId, "You must provide a clientId key");

        String clientSecret = params.get(Constants.CLIENT_SECRET);
        Preconditions.checkEmptyString(clientSecret, "You must provide a clientSecret");

        final OAuthRequest request = new OAuthRequest(verb, config.getApiURL() + Methods.SUBSCRIPTIONS);
        // Add the oauth parameter in the body
        request.addQuerystringParameter(Constants.CLIENT_ID, clientId);
        request.addQuerystringParameter(Constants.CLIENT_SECRET, clientSecret);
        configureConnectionSettings(request, config);

        return request;
    }

    private SubscriptionResponse getSubscriptionResponse(String jsonBody) throws InstagramException {
        Gson gson = new Gson();
        SubscriptionResponse response;

        try {
            response = gson.fromJson(jsonBody, SubscriptionResponse.class);
        } catch (Exception e) {
            throw new InstagramException("Error parsing json to object type ");
        }

        return response;
    }

    private SubscriptionsListResponse getSubscriptionsListResponse(String jsonBody) throws InstagramException {
        Gson gson = new Gson();
        SubscriptionsListResponse response = null;

        try {
            response = gson.fromJson(jsonBody, SubscriptionsListResponse.class);
        } catch (Exception e) {
            throw new InstagramException("Error parsing json to object type ");
        }

        return response;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("InstagramSubscription [");

        for(Map.Entry<String, String> entry: this.params.entrySet()){
        	builder.append(entry.getKey()).append('=').append(entry.getValue()).append(',');
        }
        int lastIndexOfComma = builder.lastIndexOf(",");
        builder.replace(lastIndexOfComma, lastIndexOfComma + 1, "");

        builder.append("]");
        return builder.toString();
    }

}


package org.jinstagram.http;

import org.jinstagram.utils.Preconditions;

import java.io.UnsupportedEncodingException;

import java.net.URLDecoder;
import java.net.URLEncoder;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Utils to deal with URL and url-encodings
 *
 */
public class URLUtils {
    private static final String EMPTY_STRING = "";

    private static final String PAIR_SEPARATOR = "=";

    private static final String PARAM_SEPARATOR = "&";

    private static final char QUERY_STRING_SEPARATOR = '?';

    private static final String UTF_8 = "UTF-8";

    private static final String ERROR_MSG = String.format("Cannot find specified encoding: %s", UTF_8);

    private static final Set<EncodingRule> ENCODING_RULES;

    static {
        Set<EncodingRule> rules = new HashSet<EncodingRule>();

        rules.add(new EncodingRule("*", "%2A"));
        rules.add(new EncodingRule("+", "%20"));
        rules.add(new EncodingRule("%7E", "~"));

        ENCODING_RULES = Collections.unmodifiableSet(rules);
    }

    /**
     * Encodes the passed String as UTF-8 using an algorithm that's compatible
     * with JavaScript's encodeURIComponent function. Returns
     * null if the String is null.
     *
     * @param s The String to be encoded
     * @return the encoded String
     *
     * (from: http://stackoverflow.com/questions/607176/java-equivalent-to-javascripts-encodeuricomponent-that-produces-identical-outpu)
     */
    public static String encodeURIComponent(String s)
    {
        String result = null;

        try
        {
            result = URLEncoder.encode(s, "UTF-8")
                    .replaceAll("\\+", "%20")
                    .replaceAll("\\%21", "!")
                    .replaceAll("\\%27", "'")
                    .replaceAll("\\%28", "(")
                    .replaceAll("\\%29", ")")
                    .replaceAll("\\%7E", "~");
        }

        // This exception should never occur.
        catch (UnsupportedEncodingException e)
        {
            result = s;
        }

        return result;
    }

    /**
     * Decodes the passed-in String as UTF-8 using an algorithm that's compatible
     * with JavaScript's decodeURIComponent function. Returns
     * null if the String is null.
     *
     * @param s The String to be decoded
     * @return the decoded String
     *
     * (from: http://stackoverflow.com/questions/607176/java-equivalent-to-javascripts-encodeuricomponent-that-produces-identical-outpu)
     */
    public static  String decodeURIComponent(String s) {
        String result = null;

        try {
          result = URLDecoder.decode(s, "UTF-8");
        }

        // This exception should never occur.
        catch (UnsupportedEncodingException e) {
          result = s;
        }

        return result;
    }

    /**
     * Turns a map into a form-urlencoded string
     *
     * @param map any map
     * @return form-url-encoded string
     */
    public static String formURLEncodeMap(Map<String, String> map) {
        Preconditions.checkNotNull(map, "Cannot url-encode a null object");

        return (map.size() <= 0) ? EMPTY_STRING : doFormUrlEncode(map);
    }

    private static String doFormUrlEncode(Map<String, String> map) {
        StringBuffer encodedString = new StringBuffer(map.size() * 20);

        for (Map.Entry<String, String> entry : map.entrySet()) {
            encodedString.append(PARAM_SEPARATOR).append(formURLEncode(entry.getKey()));

            if (entry.getValue() != null) {
                encodedString.append(PAIR_SEPARATOR).append(formURLEncode(entry.getValue()));
            }
        }

        return encodedString.toString().substring(1);
    }

    /**
     * Percent encodes a string
     *
     * @param string plain string
     * @return percent encoded string
     */
    public static String percentEncode(String string) {
        String encoded = formURLEncode(string);

        for (EncodingRule rule : ENCODING_RULES) {
            encoded = rule.apply(encoded);
        }

        return encoded;
    }

    /**
     * Translates a string into application/x-www-form-urlencoded format
     *
     * @param plain
     * @return form-urlencoded string
     */
    public static String formURLEncode(String string) {
        Preconditions.checkNotNull(string, "Cannot encode null string");

        try {
            return URLEncoder.encode(string, UTF_8);
        } catch (UnsupportedEncodingException uee) {
            throw new IllegalStateException(ERROR_MSG, uee);
        }
    }

    /**
     * Decodes a application/x-www-form-urlencoded string
     *
     * @param string form-urlencoded string
     * @return plain string
     */
    public static String formURLDecode(String string) {
        Preconditions.checkNotNull(string, "Cannot decode null string");

        try {
            return URLDecoder.decode(string, UTF_8);
        } catch (UnsupportedEncodingException uee) {
            throw new IllegalStateException(ERROR_MSG, uee);
        }
    }

    /**
     * Append given parameters to the query string of the url
     *
     * @param url the url to append parameters to
     * @param params any map
     * @return new url with parameters on query string
     */
    public static String appendParametersToQueryString(String url, Map<String, String> params) {
        Preconditions.checkNotNull(url, "Cannot append to null URL");

        String queryString = URLUtils.formURLEncodeMap(params);

        if (queryString.equals(EMPTY_STRING)) {
            return url;
        } else {
            url += (url.indexOf(QUERY_STRING_SEPARATOR) != -1) ? PARAM_SEPARATOR : QUERY_STRING_SEPARATOR;
            url += queryString;

            return url;
        }
    }

    /**
     * Concats a key-value map into a querystring-like String
     *
     * @param params key-value map
     * @return querystring-like String
     */
    public static String concatSortedPercentEncodedParams(Map<String, String> params) {
        StringBuilder result = new StringBuilder();

        for (Map.Entry<String, String> entry : params.entrySet()) {
            result.append(entry.getKey()).append(PAIR_SEPARATOR);
            result.append(entry.getValue()).append(PARAM_SEPARATOR);
        }

        return result.toString().substring(0, result.length() - 1);
    }

    /**
     * Parses and form-urldecodes a querystring-like string into a map
     *
     * @param queryString querystring-like String
     * @return a map with the form-urldecoded parameters
     */
    public static Map<String, String> queryStringToMap(String queryString) {
        Map<String, String> result = new HashMap<String, String>();

        if ((queryString != null) && (queryString.length() > 0)) {
            for (String param : queryString.split(PARAM_SEPARATOR)) {
                String pair[] = param.split(PAIR_SEPARATOR);
                String key = formURLDecode(pair[0]);
                String value = (pair.length > 1) ? formURLDecode(pair[1]) : EMPTY_STRING;

                result.put(key, value);
            }
        }

        return result;
    }

    private static final class EncodingRule {
        private final String ch;

        private final String toCh;

        EncodingRule(String ch, String toCh) {
            this.ch = ch;
            this.toCh = toCh;
        }

        String apply(String string) {
            return string.replace(ch, toCh);
        }
    }
}


package org.jinstagram.entity.common;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import org.jinstagram.exceptions.InstagramBadRequestException;
import org.jinstagram.exceptions.InstagramException;
import org.jinstagram.exceptions.InstagramRateLimitException;

import java.util.Map;

/**
 * A class to represents an error response from Instagram API
 *
 * @author Sachin Handiekar
 */
public class InstagramErrorResponse {

    private Meta errorMeta;

    private Map<String, String> headers;

    InstagramErrorResponse(Meta errorMeta) {
        this.errorMeta = errorMeta;
    }

    /**
     * Setter for headers field
     *
     * @param responseHeaders the response headers
     */
    public void setHeaders(Map<String, String> responseHeaders) {
        this.headers = responseHeaders;
    }

    /**
     * Throw instagram exception to the client
     *
     * @throws InstagramException
     */
    public void throwException() throws InstagramException {
        if (errorMeta != null) {
            String msg = errorMeta.getErrorType() + ": " + errorMeta.getErrorMessage();
            switch (errorMeta.getCode()) {
                case 400:
                    throw new InstagramBadRequestException(errorMeta.getErrorType(), msg, this.headers);
                case 429:
                    throw new InstagramRateLimitException(errorMeta.getErrorType(), msg, this.headers);
                default:
                    throw new InstagramException(errorMeta.getErrorType(), msg, this.headers);
            }
        } else {
            throw new InstagramException("No metadata found in response", this.headers);
        }
    }

    /**
     * Parse the specified json holding a response object. Instagram has two ways of
     * specifying an error: either a meta attribute is set or the body of the
     * response is a meta object itself.
     *
     * @param gson the gson instance to use
     * @param json the json response content
     * @return the InstagramErrorResponse object
     */
    public static InstagramErrorResponse parse(Gson gson, String json) {
        JsonElement jsonElement = gson.fromJson(json, JsonElement.class);
        JsonElement metaMember = null;
        if (jsonElement != null) {
            metaMember = jsonElement.getAsJsonObject().get("meta");
        }
        final Meta meta;
        if (metaMember != null) {
            meta = gson.fromJson(metaMember, Meta.class);
        } else {
            meta = gson.fromJson(jsonElement, Meta.class);
        }

        // Validate meta
        if (meta.getCode() != 0 && meta.getErrorType() != null) {
            return new InstagramErrorResponse(meta);
        } else {
            return new InstagramErrorResponse(null);
        }
    }
}


package com.qiniu.android.netdiag;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created by bailong on 16/2/24.
 */
public final class DNS {
    //provide by fastweb
    private static final String F_Start = "<iframe src=\"";
    private static final String F_End = ".php";

    private static final String C_Start = "
";
    private static final String C_End = "
";

    private static String getDiagUrl() throws IOException {
        String fetch = Util.httpGetString("http://ns.pbt.cloudxns.net/fast_tools/fetch_ldns_diag_client.php");
        if (fetch == null) {
            return null;
        }

        int x = fetch.indexOf(F_Start);
        if (x <= 0) {
            return null;
        }
        int y = fetch.indexOf(F_End, x + F_Start.length());
        if (y <= 0) {
            return null;
        }
        return fetch.substring(x + F_Start.length(), y + F_End.length());
    }

    public static String check() {
        String f = null;
        try {
            f = getDiagUrl();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (f == null) {
            return "get fetch url failed";
        }
        String result = null;
        try {
            result = Util.httpGetString(f);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (result == null) {
            return "check server error";
        }

        int start = result.indexOf(C_Start);
        int end = result.indexOf(C_End, start + C_Start.length());
        result = result.substring(start + C_Start.length(), end);
        result = result.replaceAll("</", "<");

        result = result.replaceAll("
", "");
        result = result.replaceAll("", "");
        result = result.replaceAll("", "");
        result = result.replaceAll("", "");
        result = result.replaceAll("", "");

        result = result.replaceAll("
", "");
        result = result.replaceAll("
", "");
        result = result.replaceAll("
", "");
        result = result.replaceAll("
", "");

        result = result.replaceAll("\n\n", "\n");
        return result.trim();
    }

    //    15ms
    private static InetAddress[] getByCommand() {
        try {
            Process process = Runtime.getRuntime().exec("getprop");
            InputStream inputStream = process.getInputStream();
            LineNumberReader lnr = new LineNumberReader(
                    new InputStreamReader(inputStream));
            String line = null;
            ArrayList<InetAddress> servers = new ArrayList<InetAddress>(5);
            while ((line = lnr.readLine()) != null) {
                int split = line.indexOf("]: [");
                if (split == -1) {
                    continue;
                }
                String property = line.substring(1, split);
                String value = line.substring(split + 4, line.length() - 1);
                if (property.endsWith(".dns") || property.endsWith(".dns1") ||
                        property.endsWith(".dns2") || property.endsWith(".dns3") ||
                        property.endsWith(".dns4")) {

                    // normalize the address

                    InetAddress ip = InetAddress.getByName(value);

                    if (ip == null) continue;

                    value = ip.getHostAddress();

                    if (value == null) continue;
                    if (value.length() == 0) continue;

                    servers.add(ip);
                }
            }
            if (servers.size() > 0) {
                return servers.toArray(new InetAddress[servers.size()]);
            }
        } catch (IOException e) {
            Logger.getLogger("AndroidDnsServer").log(Level.WARNING, "Exception in findDNSByExec", e);
        }
        return null;
    }

    // 1ms
    private static InetAddress[] getByReflection() {
        try {
            Class<?> SystemProperties =
                    Class.forName("android.os.SystemProperties");
            Method method = SystemProperties.getMethod("get",
                    new Class<?>[]{String.class});

            ArrayList<InetAddress> servers = new ArrayList<InetAddress>(5);

            for (String propKey : new String[]{
                    "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) {

                String value = (String) method.invoke(null, propKey);

                if (value == null) continue;
                if (value.length() == 0) continue;

                InetAddress ip = InetAddress.getByName(value);

                if (ip == null) continue;

                value = ip.getHostAddress();

                if (value == null) continue;
                if (value.length() == 0) continue;
                if (servers.contains(ip)) continue;

                servers.add(ip);
            }

            if (servers.size() > 0) {
                return servers.toArray(new InetAddress[servers.size()]);
            }
        } catch (Exception e) {
            // we might trigger some problems this way
            Logger.getLogger("AndroidDnsServer").log(Level.WARNING, "Exception in findDNSByReflection", e);
        }
        return null;
    }

    public static String[] local() {
        InetAddress[] addresses = getByReflection();
        if (addresses == null) {
            addresses = getByCommand();
            if (addresses == null) {
                return new String[0];
            }
        }
        String[] ret = new String[addresses.length];
        for (int i = 0; i < ret.length; i++) {
            ret[i] = addresses[i].toString();
            if (ret[i].indexOf('/') == 0) {
                ret[i] = ret[i].substring(1);
            }
        }
        return ret;
    }
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package eu.verdelhan.ta4j;

import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;

/**
 * Base implementation of a {@link Tick}.
 * <p>
 */
public class BaseTick implements Tick {

	private static final long serialVersionUID = 8038383777467488147L;
	/** Time period (e.g. 1 day, 15 min, etc.) of the tick */
    private Duration timePeriod;
    /** End time of the tick */
    private ZonedDateTime endTime;
    /** Begin time of the tick */
    private ZonedDateTime beginTime;
    /** Open price of the period */
    private Decimal openPrice = null;
    /** Close price of the period */
    private Decimal closePrice = null;
    /** Max price of the period */
    private Decimal maxPrice = null;
    /** Min price of the period */
    private Decimal minPrice = null;
    /** Traded amount during the period */
    private Decimal amount = Decimal.ZERO;
    /** Volume of the period */
    private Decimal volume = Decimal.ZERO;
    /** Trade count */
    private int trades = 0;

    /**
     * Constructor.
     * @param timePeriod the time period
     * @param endTime the end time of the tick period
     */
    public BaseTick(Duration timePeriod, ZonedDateTime endTime) {
        checkTimeArguments(timePeriod, endTime);
        this.timePeriod = timePeriod;
        this.endTime = endTime;
        this.beginTime = endTime.minus(timePeriod);
    }

    /**
     * Constructor.
     * @param endTime the end time of the tick period
     * @param openPrice the open price of the tick period
     * @param highPrice the highest price of the tick period
     * @param lowPrice the lowest price of the tick period
     * @param closePrice the close price of the tick period
     * @param volume the volume of the tick period
     */
    public BaseTick(ZonedDateTime endTime, double openPrice, double highPrice, double lowPrice, double closePrice, double volume) {
        this(endTime, Decimal.valueOf(openPrice),
                Decimal.valueOf(highPrice),
                Decimal.valueOf(lowPrice),
                Decimal.valueOf(closePrice),
                Decimal.valueOf(volume));
    }

    /**
     * Constructor.
     * @param endTime the end time of the tick period
     * @param openPrice the open price of the tick period
     * @param highPrice the highest price of the tick period
     * @param lowPrice the lowest price of the tick period
     * @param closePrice the close price of the tick period
     * @param volume the volume of the tick period
     */
    public BaseTick(ZonedDateTime endTime, String openPrice, String highPrice, String lowPrice, String closePrice, String volume) {
        this(endTime, Decimal.valueOf(openPrice),
                Decimal.valueOf(highPrice),
                Decimal.valueOf(lowPrice),
                Decimal.valueOf(closePrice),
                Decimal.valueOf(volume));
    }

    /**
     * Constructor.
     * @param endTime the end time of the tick period
     * @param openPrice the open price of the tick period
     * @param highPrice the highest price of the tick period
     * @param lowPrice the lowest price of the tick period
     * @param closePrice the close price of the tick period
     * @param volume the volume of the tick period
     */
    public BaseTick(ZonedDateTime endTime, Decimal openPrice, Decimal highPrice, Decimal lowPrice, Decimal closePrice, Decimal volume) {
        this(Duration.ofDays(1), endTime, openPrice, highPrice, lowPrice, closePrice, volume);
    }

    /**
     * Constructor.
     * @param timePeriod the time period
     * @param endTime the end time of the tick period
     * @param openPrice the open price of the tick period
     * @param highPrice the highest price of the tick period
     * @param lowPrice the lowest price of the tick period
     * @param closePrice the close price of the tick period
     * @param volume the volume of the tick period
     */
    public BaseTick(Duration timePeriod, ZonedDateTime endTime, Decimal openPrice, Decimal highPrice, Decimal lowPrice, Decimal closePrice, Decimal volume) {
        this(timePeriod, endTime, openPrice, highPrice, lowPrice, closePrice, volume, Decimal.ZERO);
    }

    /**
     * Constructor.
     * @param timePeriod the time period
     * @param endTime the end time of the tick period
     * @param openPrice the open price of the tick period
     * @param highPrice the highest price of the tick period
     * @param lowPrice the lowest price of the tick period
     * @param closePrice the close price of the tick period
     * @param volume the volume of the tick period
     * @param amount the amount of the tick period
     */
    public BaseTick(Duration timePeriod, ZonedDateTime endTime, Decimal openPrice, Decimal highPrice, Decimal lowPrice, Decimal closePrice, Decimal volume, Decimal amount) {
        checkTimeArguments(timePeriod, endTime);
        this.timePeriod = timePeriod;
        this.endTime = endTime;
        this.beginTime = endTime.minus(timePeriod);
        this.openPrice = openPrice;
        this.maxPrice = highPrice;
        this.minPrice = lowPrice;
        this.closePrice = closePrice;
        this.volume = volume;
        this.amount = amount;
    }

    /**
     * @return the open price of the period
     */
    public Decimal getOpenPrice() {
        return openPrice;
    }

    /**
     * @return the min price of the period
     */
    public Decimal getMinPrice() {
        return minPrice;
    }

    /**
     * @return the max price of the period
     */
    public Decimal getMaxPrice() {
        return maxPrice;
    }

    /**
     * @return the close price of the period
     */
    public Decimal getClosePrice() {
        return closePrice;
    }

    /**
     * @return the whole traded volume in the period
     */
    public Decimal getVolume() {
        return volume;
    }

    /**
     * @return the number of trades in the period
     */
    public int getTrades() {
        return trades;
    }

    /**
     * @return the whole traded amount of the period
     */
    public Decimal getAmount() {
        return amount;
    }

    /**
     * @return the time period of the tick
     */
    public Duration getTimePeriod() {
        return timePeriod;
    }

    /**
     * @return the begin timestamp of the tick period
     */
    public ZonedDateTime getBeginTime() {
        return beginTime;
    }

    /**
     * @return the end timestamp of the tick period
     */
    public ZonedDateTime getEndTime() {
        return endTime;
    }

    /**
     * Adds a trade at the end of tick period.
     * @param tradeVolume the traded volume
     * @param tradePrice the price
     */
    public void addTrade(Decimal tradeVolume, Decimal tradePrice) {
        if (openPrice == null) {
            openPrice = tradePrice;
        }
        closePrice = tradePrice;

        if (maxPrice == null) {
            maxPrice = tradePrice;
        } else {
            maxPrice = maxPrice.isLessThan(tradePrice) ? tradePrice : maxPrice;
        }
        if (minPrice == null) {
            minPrice = tradePrice;
        } else {
            minPrice = minPrice.isGreaterThan(tradePrice) ? tradePrice : minPrice;
        }
        volume = volume.plus(tradeVolume);
        amount = amount.plus(tradeVolume.multipliedBy(tradePrice));
        trades++;
    }

    @Override
    public String toString() {
        return String.format("{end time: %1s, close price: %2$f, open price: %3$f, min price: %4$f, max price: %5$f, volume: %6$f}",
                endTime.withZoneSameInstant(ZoneId.systemDefault()), closePrice.toDouble(), openPrice.toDouble(), minPrice.toDouble(), maxPrice.toDouble(), volume.toDouble());
    }

    /**
     * @param timePeriod the time period
     * @param endTime the end time of the tick
     * @throws IllegalArgumentException if one of the arguments is null
     */
    private void checkTimeArguments(Duration timePeriod, ZonedDateTime endTime) {
        if (timePeriod == null) {
            throw new IllegalArgumentException("Time period cannot be null");
        }
        if (endTime == null) {
            throw new IllegalArgumentException("End time cannot be null");
        }
    }
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package eu.verdelhan.ta4j.indicators;

import eu.verdelhan.ta4j.Indicator;
import eu.verdelhan.ta4j.TimeSeries;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Cached {@link Indicator indicator}.
 * <p>
 * Caches the constructor of the indicator. Avoid to calculate the same index of the indicator twice.
 */
public abstract class CachedIndicator<T> extends AbstractIndicator<T> {

    /** List of cached results */
    private final List<T> results = new ArrayList<T>();

    /**
     * Should always be the index of the last result in the results list.
     * I.E. the last calculated result.
     */
    protected int highestResultIndex = -1;

    /**
     * Constructor.
     * @param series the related time series
     */
    public CachedIndicator(TimeSeries series) {
        super(series);
    }

    /**
     * Constructor.
     * @param indicator a related indicator (with a time series)
     */
    public CachedIndicator(Indicator indicator) {
        this(indicator.getTimeSeries());
    }

    @Override
    public T getValue(int index) {
        TimeSeries series = getTimeSeries();
        if (series == null) {
            // Series is null; the indicator doesn't need cache.
            // (e.g. simple computation of the value)
            // --> Calculating the value
            return calculate(index);
        }

        // Series is not null

        final int removedTicksCount = series.getRemovedTicksCount();
        final int maximumResultCount = series.getMaximumTickCount();

        T result;
        if (index < removedTicksCount) {
            // Result already removed from cache
            log.trace("{}: result from tick {} already removed from cache, use {}-th instead",
                    getClass().getSimpleName(), index, removedTicksCount);
            increaseLengthTo(removedTicksCount, maximumResultCount);
            highestResultIndex = removedTicksCount;
            result = results.get(0);
            if (result == null) {
                // It should be "result = calculate(removedTicksCount);".
                // We use "result = calculate(0);" as a workaround
                // to fix issue #120 (https://github.com/mdeverdelhan/ta4j/issues/120).
                result = calculate(0);
                results.set(0, result);
            }
        } else {
            increaseLengthTo(index, maximumResultCount);
            if (index > highestResultIndex) {
                // Result not calculated yet
                highestResultIndex = index;
                result = calculate(index);
                results.set(results.size()-1, result);
            } else {
                // Result covered by current cache
                int resultInnerIndex = results.size() - 1 - (highestResultIndex - index);
                result = results.get(resultInnerIndex);
                if (result == null) {
                    result = calculate(index);
                }
                results.set(resultInnerIndex, result);
            }
        }
        return result;
    }

    /**
     * @param index the tick index
     * @return the value of the indicator
     */
    protected abstract T calculate(int index);

    /**
     * Increases the size of cached results buffer.
     * @param index the index to increase length to
     * @param maxLength the maximum length of the results buffer
     */
    private void increaseLengthTo(int index, int maxLength) {
        if (highestResultIndex > -1) {
            int newResultsCount = Math.min(index-highestResultIndex, maxLength);
            if (newResultsCount == maxLength) {
                results.clear();
                results.addAll(Collections.<T> nCopies(maxLength, null));
            } else if (newResultsCount > 0) {
                results.addAll(Collections.<T> nCopies(newResultsCount, null));
                removeExceedingResults(maxLength);
            }
        } else {
            // First use of cache
            assert results.isEmpty() : "Cache results list should be empty";
            results.addAll(Collections.<T> nCopies(Math.min(index+1, maxLength), null));
        }
    }

    /**
     * Removes the N first results which exceed the maximum tick count.
     * (i.e. keeps only the last maximumResultCount results)
     * @param maximumResultCount the number of results to keep
     */
    private void removeExceedingResults(int maximumResultCount) {
        int resultCount = results.size();
        if (resultCount > maximumResultCount) {
            // Removing old results
            final int nbResultsToRemove = resultCount - maximumResultCount;
            for (int i = 0; i < nbResultsToRemove; i++) {
                results.remove(0);
            }
        }
    }
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package eu.verdelhan.ta4j.analysis.criteria;

import eu.verdelhan.ta4j.Decimal;
import eu.verdelhan.ta4j.TimeSeries;
import eu.verdelhan.ta4j.Trade;
import eu.verdelhan.ta4j.TradingRecord;
import eu.verdelhan.ta4j.analysis.CashFlow;

/**
 * Maximum drawdown criterion.
 *

 * @see http://en.wikipedia.org/wiki/Drawdown_%28economics%29
 */
public class MaximumDrawdownCriterion extends AbstractAnalysisCriterion {

    @Override
    public double calculate(TimeSeries series, TradingRecord tradingRecord) {
        CashFlow cashFlow = new CashFlow(series, tradingRecord);
        Decimal maximumDrawdown = calculateMaximumDrawdown(series, cashFlow);
        return maximumDrawdown.toDouble();
    }

    @Override
    public double calculate(TimeSeries series, Trade trade) {
        if (trade != null && trade.getEntry() != null && trade.getExit() != null) {
            CashFlow cashFlow = new CashFlow(series, trade);
            Decimal maximumDrawdown = calculateMaximumDrawdown(series, cashFlow);
            return maximumDrawdown.toDouble();
        }
        return 0;
    }

    @Override
    public boolean betterThan(double criterionValue1, double criterionValue2) {
        return criterionValue1 < criterionValue2;
    }

    /**
     * Calculates the maximum drawdown from a cash flow over a series.
     * @param series the time series
     * @param cashFlow the cash flow
     * @return the maximum drawdown from a cash flow over a series
     */
    private Decimal calculateMaximumDrawdown(TimeSeries series, CashFlow cashFlow) {
        Decimal maximumDrawdown = Decimal.ZERO;
        Decimal maxPeak = Decimal.ZERO;
        if (!series.isEmpty()) {
        	// The series is not empty
	        for (int i = series.getBeginIndex(); i <= series.getEndIndex(); i++) {
	            Decimal value = cashFlow.getValue(i);
	            if (value.isGreaterThan(maxPeak)) {
	                maxPeak = value;
	            }

	            Decimal drawdown = maxPeak.minus(value).dividedBy(maxPeak);
	            if (drawdown.isGreaterThan(maximumDrawdown)) {
	                maximumDrawdown = drawdown;
	            }
	        }
        }
        return maximumDrawdown;
    }
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package eu.verdelhan.ta4j;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.Objects;

/**
 * Immutable, arbitrary-precision signed decimal numbers designed for technical analysis.
 *

 * A {@code Decimal} consists of a {@code BigDecimal} with arbitrary {@link MathContext} (precision and rounding mode).
 *
 * @see BigDecimal
 * @see MathContext
 * @see RoundingMode
 */
public final class Decimal implements Comparable<Decimal>, Serializable {

	private static final long serialVersionUID = 2225130444465033658L;

	public static final MathContext MATH_CONTEXT = new MathContext(32, RoundingMode.HALF_UP);

    /** Not-a-Number instance (infinite error) */
    public static final Decimal NaN = new Decimal();

    public static final Decimal ZERO = valueOf(0);
    public static final Decimal ONE = valueOf(1);
    public static final Decimal TWO = valueOf(2);
    public static final Decimal THREE = valueOf(3);
    public static final Decimal TEN = valueOf(10);
    public static final Decimal HUNDRED = valueOf(100);
    public static final Decimal THOUSAND = valueOf(1000);

    private final BigDecimal delegate;

    /**
     * Constructor.
     * Only used for NaN instance.
     */
    private Decimal() {
        delegate = null;
    }

    /**
     * Constructor.
     * @param val the string representation of the decimal value
     */
    private Decimal(String val) {
        delegate = new BigDecimal(val, MATH_CONTEXT);
    }

    /**
     * Constructor.
     * @param val the double value
     */
    private Decimal(double val) {
        delegate = new BigDecimal(val, MATH_CONTEXT);
    }

    private Decimal(int val) {
        delegate = new BigDecimal(val, MATH_CONTEXT);
    }

    private Decimal(long val) {
        delegate = new BigDecimal(val, MATH_CONTEXT);
    }

    private Decimal(BigDecimal val) {
        delegate = val;
    }

    /**
     * Returns a {@code Decimal} whose value is {@code (this + augend)},
     * with rounding according to the context settings.
     * @param augend value to be added to this {@code Decimal}.
     * @return {@code this + augend}, rounded as necessary
     * @see BigDecimal#add(java.math.BigDecimal, java.math.MathContext)
     */
    public Decimal plus(Decimal augend) {
        if ((this == NaN) || (augend == NaN)) {
            return NaN;
        }
        return new Decimal(delegate.add(augend.delegate, MATH_CONTEXT));
    }

    /**
     * Returns a {@code Decimal} whose value is {@code (this - augend)},
     * with rounding according to the context settings.
     * @param subtrahend value to be subtracted from this {@code Decimal}.
     * @return {@code this - subtrahend}, rounded as necessary
     * @see BigDecimal#subtract(java.math.BigDecimal, java.math.MathContext)
     */
    public Decimal minus(Decimal subtrahend) {
        if ((this == NaN) || (subtrahend == NaN)) {
            return NaN;
        }
        return new Decimal(delegate.subtract(subtrahend.delegate, MATH_CONTEXT));
    }

    /**
     * Returns a {@code Decimal} whose value is {@code this * multiplicand},
     * with rounding according to the context settings.
     * @param multiplicand value to be multiplied by this {@code Decimal}.
     * @return {@code this * multiplicand}, rounded as necessary
     * @see BigDecimal#multiply(java.math.BigDecimal, java.math.MathContext)
     */
    public Decimal multipliedBy(Decimal multiplicand) {
        if ((this == NaN) || (multiplicand == NaN)) {
            return NaN;
        }
        return new Decimal(delegate.multiply(multiplicand.delegate, MATH_CONTEXT));
    }

    /**
     * Returns a {@code Decimal} whose value is {@code (this / divisor)},
     * with rounding according to the context settings.
     * @param divisor value by which this {@code Decimal} is to be divided.
     * @return {@code this / divisor}, rounded as necessary
     * @see BigDecimal#divide(java.math.BigDecimal, java.math.MathContext)
     */
    public Decimal dividedBy(Decimal divisor) {
        if ((this == NaN) || (divisor == NaN) || divisor.isZero()) {
            return NaN;
        }
        return new Decimal(delegate.divide(divisor.delegate, MATH_CONTEXT));
    }

    /**
     * Returns a {@code Decimal} whose value is {@code (this % divisor)},
     * with rounding according to the context settings.
     * @param divisor value by which this {@code Decimal} is to be divided.
     * @return {@code this % divisor}, rounded as necessary.
     * @see BigDecimal#remainder(java.math.BigDecimal, java.math.MathContext)
     */
    public Decimal remainder(Decimal divisor) {
        if ((this == NaN) || (divisor == NaN) || divisor.isZero()) {
            return NaN;
        }
        return new Decimal(delegate.remainder(divisor.delegate, MATH_CONTEXT));
    }

    /**
     * Returns a {@code Decimal} whose value is <tt>(thisn)</tt>.
     * @param n power to raise this {@code Decimal} to.
     * @return <tt>thisn</tt>
     * @see BigDecimal#pow(int, java.math.MathContext)
     */
    public Decimal pow(int n) {
        if (this == NaN) {
            return NaN;
        }
        return new Decimal(delegate.pow(n, MATH_CONTEXT));
    }

    /**
     * Returns the correctly rounded natural logarithm (base e) of the double value of this {@code Decimal}.
     * /!\ Warning! Uses the {@code StrictMath#log(double)} method under the hood.
     * @return the natural logarithm (base e) of {@code this}
     * @see StrictMath#log(double)
     */
    public Decimal log() {
        if (this == NaN) {
            return NaN;
        }
        return new Decimal(StrictMath.log(delegate.doubleValue()));
    }

    /**
     * Returns the correctly rounded positive square root of the double value of this {@code Decimal}.
     * /!\ Warning! Uses the {@code StrictMath#sqrt(double)} method under the hood.
     * @return the positive square root of {@code this}
     * @see StrictMath#sqrt(double)
     */
    public Decimal sqrt() {
        if (this == NaN) {
            return NaN;
        }
        return new Decimal(StrictMath.sqrt(delegate.doubleValue()));
    }

    /**
     * Returns a {@code Decimal} whose value is the absolute value
     * of this {@code Decimal}.
     * @return {@code abs(this)}
     */
    public Decimal abs() {
        if (this == NaN) {
            return NaN;
        }
        return new Decimal(delegate.abs());
    }

    /**
     * Checks if the value is zero.
     * @return true if the value is zero, false otherwise
     */
    public boolean isZero() {
        if (this == NaN) {
            return false;
        }
        return compareTo(ZERO) == 0;
    }

    /**
     * Checks if the value is greater than zero.
     * @return true if the value is greater than zero, false otherwise
     */
    public boolean isPositive() {
        if (this == NaN) {
            return false;
        }
        return compareTo(ZERO) > 0;
    }

    /**
     * Checks if the value is zero or greater.
     * @return true if the value is zero or greater, false otherwise
     */
    public boolean isPositiveOrZero() {
        if (this == NaN) {
            return false;
        }
        return compareTo(ZERO) >= 0;
    }

    /**
     * Checks if the value is Not-a-Number.
     * @return true if the value is Not-a-Number (NaN), false otherwise
     */
    public boolean isNaN() {
        return this == NaN;
    }

    /**
     * Checks if the value is less than zero.
     * @return true if the value is less than zero, false otherwise
     */
    public boolean isNegative() {
        if (this == NaN) {
            return false;
        }
        return compareTo(ZERO) < 0;
    }

    /**
     * Checks if the value is zero or less.
     * @return true if the value is zero or less, false otherwise
     */
    public boolean isNegativeOrZero() {
        if (this == NaN) {
            return false;
        }
        return compareTo(ZERO) <= 0;
    }

    /**
     * Checks if this value is equal to another.
     * @param other the other value, not null
     * @return true is this is greater than the specified value, false otherwise
     */
    public boolean isEqual(Decimal other) {
        if ((this == NaN) || (other == NaN)) {
            return false;
        }
        return compareTo(other) == 0;
    }

    /**
     * Checks if this value is greater than another.
     * @param other the other value, not null
     * @return true is this is greater than the specified value, false otherwise
     */
    public boolean isGreaterThan(Decimal other) {
        if ((this == NaN) || (other == NaN)) {
            return false;
        }
        return compareTo(other) > 0;
    }

    /**
     * Checks if this value is greater than or equal to another.
     * @param other the other value, not null
     * @return true is this is greater than or equal to the specified value, false otherwise
     */
    public boolean isGreaterThanOrEqual(Decimal other) {
        if ((this == NaN) || (other == NaN)) {
            return false;
        }
        return compareTo(other) > -1;
    }

    /**
     * Checks if this value is less than another.
     * @param other the other value, not null
     * @return true is this is less than the specified value, false otherwise
     */
    public boolean isLessThan(Decimal other) {
        if ((this == NaN) || (other == NaN)) {
            return false;
        }
        return compareTo(other) < 0;
    }

    /**
     * Checks if this value is less than or equal to another.
     * @param other the other value, not null
     * @return true is this is less than or equal to the specified value, false otherwise
     */
    public boolean isLessThanOrEqual(Decimal other) {
        if ((this == NaN) || (other == NaN)) {
            return false;
        }
        return compareTo(other) < 1;
    }

    @Override
    public int compareTo(Decimal other) {
        if ((this == NaN) || (other == NaN)) {
            return 0;
        }
        return delegate.compareTo(other.delegate);
    }

    /**
     * Returns the minimum of this {@code Decimal} and {@code other}.
     * @param other value with which the minimum is to be computed
     * @return the {@code Decimal} whose value is the lesser of this
     *         {@code Decimal} and {@code other}.  If they are equal,
     *         as defined by the {@link #compareTo(Decimal) compareTo}
     *         method, {@code this} is returned.
     */
    public Decimal min(Decimal other) {
        if ((this == NaN) || (other == NaN)) {
            return NaN;
        }
        return (compareTo(other) <= 0 ? this : other);
    }

    /**
     * Returns the maximum of this {@code Decimal} and {@code other}.
     * @param  other value with which the maximum is to be computed
     * @return the {@code Decimal} whose value is the greater of this
     *         {@code Decimal} and {@code other}.  If they are equal,
     *         as defined by the {@link #compareTo(Decimal) compareTo}
     *         method, {@code this} is returned.
     */
    public Decimal max(Decimal other) {
        if ((this == NaN) || (other == NaN)) {
            return NaN;
        }
        return (compareTo(other) >= 0 ? this : other);
    }

    /**
     * Converts this {@code Decimal} to a {@code double}.
     * @return this {@code Decimal} converted to a {@code double}
     * @see BigDecimal#doubleValue()
     */
    public double toDouble() {
        if (this == NaN) {
            return Double.NaN;
        }
        return delegate.doubleValue();
    }

    @Override
    public String toString() {
        if (this == NaN) {
            return "NaN";
        }
        return delegate.toString();
    }

    @Override
    public int hashCode() {
        return Objects.hash(delegate);
    }

    /**
     * {@inheritDoc}
     * Warning: This method returns true if `this` and `obj` are both NaN.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof Decimal)) {
            return false;
        }
        final Decimal other = (Decimal) obj;
        if (this.delegate != other.delegate
                && (this.delegate == null || (this.delegate.compareTo(other.delegate) != 0))) {
            return false;
        }
        return true;
    }

    public static Decimal valueOf(String val) {
        if ("NaN".equals(val)) {
            return NaN;
        }
        return new Decimal(val);
    }

    public static Decimal valueOf(double val) {
        if (Double.isNaN(val)) {
            return NaN;
        }
        return new Decimal(val);
    }

    public static Decimal valueOf(int val) {
        return new Decimal(val);
    }

    public static Decimal valueOf(long val) {
        return new Decimal(val);
    }
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package eu.verdelhan.ta4j.indicators.statistics;

import eu.verdelhan.ta4j.Indicator;
import eu.verdelhan.ta4j.Decimal;
import eu.verdelhan.ta4j.indicators.CachedIndicator;

/**
 * Periodical Growth Rate indicator.
 *
 * In general the 'Growth Rate' is useful for comparing the average returns of
 * investments in stocks or funds and can be used to compare the performance
 * e.g. comparing the historical returns of stocks with bonds.
 *
 * This indicator has the following characteristics:
 *  - the calculation is timeframe dependendant. The timeframe corresponds to the
 *    number of trading events in a period, e. g. the timeframe for a US trading
 *    year for end of day ticks would be '251' trading days
 *  - the result is a step function with a constant value within a timeframe
 *  - NaN values while index is smaller than timeframe, e.g. timeframe is year,
 *    than no values are calculated before a full year is reached
 *  - NaN values for incomplete timeframes, e.g. timeframe is a year and your
 *    timeseries contains data for 11,3 years, than no values are calculated for
 *    the remaining 0,3 years
 *  - the method 'getTotalReturn' calculates the total return over all returns
 *    of the coresponding timeframes
 *
 *
 * Further readings:
 * Good sumary on 'Rate of Return': https://en.wikipedia.org/wiki/Rate_of_return
 * Annual return / CAGR: http://www.investopedia.com/terms/a/annual-return.asp
 * Annualized Total Return: http://www.investopedia.com/terms/a/annualized-total-return.asp
 * Annualized Return vs. Cumulative Return:
 * http://www.fool.com/knowledge-center/2015/11/03/annualized-return-vs-cumulative-return.aspx
 *
 */
public class PeriodicalGrowthRateIndicator extends CachedIndicator<Decimal> {

    private final Indicator<Decimal> indicator;

    private final int timeFrame;

    /**
     * Constructor.
     * Example: use timeFrame = 251 and "end of day"-ticks for annual behaviour
     * in the US (http://tradingsim.com/blog/trading-days-in-a-year/).
     * @param indicator
     * @param timeFrame
     */
    public PeriodicalGrowthRateIndicator(Indicator<Decimal> indicator, int timeFrame) {
        super(indicator);
        this.indicator = indicator;
        this.timeFrame = timeFrame;
    }

    /**
     * Gets the TotalReturn from the calculated results of the method 'calculate'.
     * For a timeFrame = number of trading days within a year (e. g. 251 days in the US)
     * and "end of day"-ticks you will get the 'Annualized Total Return'.
     * Only complete timeFrames are taken into the calculation.
     * @return the total return from the calculated results of the method 'calculate'
     */
    public double getTotalReturn() {

        Decimal totalProduct = Decimal.ONE;
        int completeTimeframes = (getTimeSeries().getTickCount() / timeFrame);

        for (int i = 1; i <= completeTimeframes; i++) {
            int index = i * timeFrame;
            Decimal currentReturn = getValue(index);

            // Skip NaN at the end of a series
            if (currentReturn != Decimal.NaN) {
                currentReturn = currentReturn.plus(Decimal.ONE);
                totalProduct = totalProduct.multipliedBy(currentReturn);
            }
        }

        return (Math.pow(totalProduct.toDouble(), (1.0 / completeTimeframes)));
    }

    @Override
    protected Decimal calculate(int index) {

        Decimal currentValue = indicator.getValue(index);

        int helpPartialTimeframe = index % timeFrame;
        double helpFullTimeframes = Math.floor((double) indicator.getTimeSeries().getTickCount() / (double) timeFrame);
        double helpIndexTimeframes = (double) index / (double) timeFrame;

        double helpPartialTimeframeHeld = (double) helpPartialTimeframe / (double) timeFrame;
        double partialTimeframeHeld = (helpPartialTimeframeHeld == 0) ? 1.0 : helpPartialTimeframeHeld;

        // Avoid calculations of returns:
        // a.) if index number is below timeframe
        // e.g. timeframe = 365, index = 5 => no calculation
        // b.) if at the end of a series incomplete timeframes would remain
        Decimal timeframedReturn = Decimal.NaN;
        if ((index >= timeFrame) /*(a)*/ && (helpIndexTimeframes < helpFullTimeframes) /*(b)*/) {
            Decimal movingValue = indicator.getValue(index - timeFrame);
            Decimal movingSimpleReturn = (currentValue.minus(movingValue)).dividedBy(movingValue);

            double timeframedReturn_double = Math.pow((1 + movingSimpleReturn.toDouble()), (1 / partialTimeframeHeld)) - 1;
            timeframedReturn = Decimal.valueOf(timeframedReturn_double);
        }

        return timeframedReturn;

    }
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package ta4jexamples.walkforward;

import eu.verdelhan.ta4j.AnalysisCriterion;
import eu.verdelhan.ta4j.BaseTimeSeries;
import eu.verdelhan.ta4j.Strategy;
import eu.verdelhan.ta4j.TimeSeries;
import eu.verdelhan.ta4j.TimeSeriesManager;
import eu.verdelhan.ta4j.TradingRecord;
import eu.verdelhan.ta4j.analysis.criteria.TotalProfitCriterion;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.time.Duration;
import java.time.ZonedDateTime;

import ta4jexamples.loaders.CsvTradesLoader;
import ta4jexamples.strategies.CCICorrectionStrategy;
import ta4jexamples.strategies.GlobalExtremaStrategy;
import ta4jexamples.strategies.MovingMomentumStrategy;
import ta4jexamples.strategies.RSI2Strategy;

/**
 * Walk-forward optimization example.
 *

 * @see http://en.wikipedia.org/wiki/Walk_forward_optimization
 * @see http://www.futuresmag.com/2010/04/01/can-your-system-do-the-walk
 */
public class WalkForward {

    /**
     * Builds a list of split indexes from splitDuration.
     * @param series the time series to get split begin indexes of
     * @param splitDuration the duration between 2 splits
     * @return a list of begin indexes after split
     */
    public static List<Integer> getSplitBeginIndexes(TimeSeries series, Duration splitDuration) {
        ArrayList<Integer> beginIndexes = new ArrayList<>();

        int beginIndex = series.getBeginIndex();
        int endIndex = series.getEndIndex();

        // Adding the first begin index
        beginIndexes.add(beginIndex);

        // Building the first interval before next split
        ZonedDateTime beginInterval = series.getFirstTick().getEndTime();
        ZonedDateTime endInterval = beginInterval.plus(splitDuration);

        for (int i = beginIndex; i <= endIndex; i++) {
            // For each tick...
            ZonedDateTime tickTime = series.getTick(i).getEndTime();
            if (tickTime.isBefore(beginInterval) || !tickTime.isBefore(endInterval)) {
                // Tick out of the interval
                if (!endInterval.isAfter(tickTime)) {
                    // Tick after the interval
                    // --> Adding a new begin index
                    beginIndexes.add(i);
                }

                // Building the new interval before next split
                beginInterval = endInterval.isBefore(tickTime) ? tickTime : endInterval;
                endInterval = beginInterval.plus(splitDuration);
            }
        }
        return beginIndexes;
    }

    /**
     * Returns a new time series which is a view of a subset of the current series.
     *

     * The new series has begin and end indexes which correspond to the bounds of the sub-set into the full series.

     * The tick of the series are shared between the original time series and the returned one (i.e. no copy).
     * @param series the time series to get a sub-series of
     * @param beginIndex the begin index (inclusive) of the time series
     * @param duration the duration of the time series
     * @return a constrained {@link TimeSeries time series} which is a sub-set of the current series
     */
    public static TimeSeries subseries(TimeSeries series, int beginIndex, Duration duration) {

        // Calculating the sub-series interval
        ZonedDateTime beginInterval = series.getTick(beginIndex).getEndTime();
        ZonedDateTime endInterval = beginInterval.plus(duration);

        // Checking ticks belonging to the sub-series (starting at the provided index)
        int subseriesNbTicks = 0;
        int endIndex = series.getEndIndex();
        for (int i = beginIndex; i <= endIndex; i++) {
            // For each tick...
            ZonedDateTime tickTime = series.getTick(i).getEndTime();
            if (tickTime.isBefore(beginInterval) || !tickTime.isBefore(endInterval)) {
                // Tick out of the interval
                break;
            }
            // Tick in the interval
            // --> Incrementing the number of ticks in the subseries
            subseriesNbTicks++;
        }

        return new BaseTimeSeries(series, beginIndex, beginIndex + subseriesNbTicks - 1);
    }

    /**
     * Splits the time series into sub-series lasting sliceDuration.

     * The current time series is splitted every splitDuration.

     * The last sub-series may last less than sliceDuration.
     * @param series the time series to split
     * @param splitDuration the duration between 2 splits
     * @param sliceDuration the duration of each sub-series
     * @return a list of sub-series
     */
    public static List<TimeSeries> splitSeries(TimeSeries series, Duration splitDuration, Duration sliceDuration) {
        ArrayList<TimeSeries> subseries = new ArrayList<>();
        if (splitDuration != null && !splitDuration.isZero()
                && sliceDuration != null && !sliceDuration.isZero()) {

            List<Integer> beginIndexes = getSplitBeginIndexes(series, splitDuration);
            for (Integer subseriesBegin : beginIndexes) {
                subseries.add(subseries(series, subseriesBegin, sliceDuration));
            }
        }
        return subseries;
    }

    /**
     * @param series the time series
     * @return a map (key: strategy, value: name) of trading strategies
     */
    public static Map<Strategy, String> buildStrategiesMap(TimeSeries series) {
        HashMap<Strategy, String> strategies = new HashMap<>();
        strategies.put(CCICorrectionStrategy.buildStrategy(series), "CCI Correction");
        strategies.put(GlobalExtremaStrategy.buildStrategy(series), "Global Extrema");
        strategies.put(MovingMomentumStrategy.buildStrategy(series), "Moving Momentum");
        strategies.put(RSI2Strategy.buildStrategy(series), "RSI-2");
        return strategies;
    }

    public static void main(String[] args) {
        // Splitting the series into slices
        TimeSeries series = CsvTradesLoader.loadBitstampSeries();
        List<TimeSeries> subseries = splitSeries(series, Duration.ofHours(6), Duration.ofDays(7));

        // Building the map of strategies
        Map<Strategy, String> strategies = buildStrategiesMap(series);

        // The analysis criterion
        AnalysisCriterion profitCriterion = new TotalProfitCriterion();

        for (TimeSeries slice : subseries) {
            // For each sub-series...
            System.out.println("Sub-series: " + slice.getSeriesPeriodDescription());
            TimeSeriesManager sliceManager = new TimeSeriesManager(slice);
            for (Map.Entry<Strategy, String> entry : strategies.entrySet()) {
                Strategy strategy = entry.getKey();
                String name = entry.getValue();
                // For each strategy...
                TradingRecord tradingRecord = sliceManager.run(strategy);
                double profit = profitCriterion.calculate(slice, tradingRecord);
                System.out.println("\tProfit for " + name + ": " + profit);
            }
            Strategy bestStrategy = profitCriterion.chooseBest(sliceManager, new ArrayList<Strategy>(strategies.keySet()));
            System.out.println("\t\t--> Best strategy: " + strategies.get(bestStrategy) + "\n");
        }
    }

}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package eu.verdelhan.ta4j.indicators.statistics;

import eu.verdelhan.ta4j.Decimal;
import eu.verdelhan.ta4j.Indicator;
import eu.verdelhan.ta4j.indicators.CachedIndicator;

/**
 * Simple linear regression indicator.
 * <p>
 * A moving (i.e. over the time frame) simple linear regression (least squares).
 * y = slope * x + intercept
 * See also: http://introcs.cs.princeton.edu/java/97data/LinearRegression.java.html
 */
public class SimpleLinearRegressionIndicator extends CachedIndicator<Decimal> {

    private Indicator<Decimal> indicator;

    private int timeFrame;

    private Decimal slope;

    private Decimal intercept;

    public SimpleLinearRegressionIndicator(Indicator<Decimal> indicator, int timeFrame) {
        super(indicator);
        this.indicator = indicator;
        this.timeFrame = timeFrame;
    }

    @Override
    protected Decimal calculate(int index) {
        final int startIndex = Math.max(0, index - timeFrame + 1);
        final int endIndex = index;
        if (endIndex - startIndex + 1 < 2) {
            // Not enough observations to compute a regression line
            return Decimal.NaN;
        }
        calculateRegressionLine(startIndex, endIndex);
        return slope.multipliedBy(Decimal.valueOf(index)).plus(intercept);
    }

    /**
     * Calculates the regression line.
     * @param startIndex the start index (inclusive) in the time series
     * @param endIndex the end index (inclusive) in the time series
     */
    private void calculateRegressionLine(int startIndex, int endIndex) {
        // First pass: compute xBar and yBar
        Decimal sumX = Decimal.ZERO;
        Decimal sumY = Decimal.ZERO;
        for (int i = startIndex; i <= endIndex; i++) {
            sumX = sumX.plus(Decimal.valueOf(i));
            sumY = sumY.plus(indicator.getValue(i));
        }
        Decimal nbObservations = Decimal.valueOf(endIndex - startIndex + 1);
        Decimal xBar = sumX.dividedBy(nbObservations);
        Decimal yBar = sumY.dividedBy(nbObservations);

        // Second pass: compute slope and intercept
        Decimal xxBar = Decimal.ZERO;
        Decimal xyBar = Decimal.ZERO;
        for (int i = startIndex; i <= endIndex; i++) {
            Decimal dX = Decimal.valueOf(i).minus(xBar);
            Decimal dY = indicator.getValue(i).minus(yBar);
            xxBar = xxBar.plus(dX.multipliedBy(dX));
            xyBar = xyBar.plus(dX.multipliedBy(dY));
        }

        slope = xyBar.dividedBy(xxBar);
        intercept = yBar.minus(slope.multipliedBy(xBar));
    }
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package eu.verdelhan.ta4j;

import java.io.Serializable;
import java.util.Objects;

import eu.verdelhan.ta4j.Order.OrderType;

/**
 * Pair of two {@link Order orders}.
 *

 * The exit order has the complement type of the entry order.

 * I.e.:
 *   entry == BUY --> exit == SELL
 *   entry == SELL --> exit == BUY
 */
public class Trade implements Serializable {

	private static final long serialVersionUID = -5484709075767220358L;

	/** The entry order */
    private Order entry;

    /** The exit order */
    private Order exit;

    /** The type of the entry order */
    private OrderType startingType;

    /**
     * Constructor.
     */
    public Trade() {
        this(OrderType.BUY);
    }

    /**
     * Constructor.
     * @param startingType the starting {@link OrderType order type} of the trade (i.e. type of the entry order)
     */
    public Trade(OrderType startingType) {
        if (startingType == null) {
            throw new IllegalArgumentException("Starting type must not be null");
        }
        this.startingType = startingType;
    }

    /**
     * Constructor.
     * @param entry the entry {@link Order order}
     * @param exit the exit {@link Order order}
     */
    public Trade(Order entry, Order exit) {
        if (entry.getType().equals(exit.getType())) {
            throw new IllegalArgumentException("Both orders must have different types");
        }
        this.startingType = entry.getType();
        this.entry = entry;
        this.exit = exit;
    }

    /**
     * @return the entry {@link Order order} of the trade
     */
    public Order getEntry() {
        return entry;
    }

    /**
     * @return the exit {@link Order order} of the trade
     */
    public Order getExit() {
        return exit;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Trade) {
            Trade t = (Trade) obj;
            return entry.equals(t.getEntry()) && exit.equals(t.getExit());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(entry, exit);
    }

    /**
     * Operates the trade at the index-th position
     * @param index the tick index
     * @return the order
     */
    public Order operate(int index) {
        return operate(index, Decimal.NaN, Decimal.NaN);
    }

    /**
     * Operates the trade at the index-th position
     * @param index the tick index
     * @param price the price
     * @param amount the amount
     * @return the order
     */
    public Order operate(int index, Decimal price, Decimal amount) {
        Order order = null;
        if (isNew()) {
            order = new Order(index, startingType, price, amount);
            entry = order;
        } else if (isOpened()) {
            if (index < entry.getIndex()) {
                throw new IllegalStateException("The index i is less than the entryOrder index");
            }
            order = new Order(index, startingType.complementType(), price, amount);
            exit = order;
        }
        return order;
    }

    /**
     * @return true if the trade is closed, false otherwise
     */
    public boolean isClosed() {
        return (entry != null) && (exit != null);
    }

    /**
     * @return true if the trade is opened, false otherwise
     */
    public boolean isOpened() {
        return (entry != null) && (exit == null);
    }

    /**
     * @return true if the trade is new, false otherwise
     */
    public boolean isNew() {
        return (entry == null) && (exit == null);
    }

    @Override
    public String toString() {
        return "Entry: " + entry + " exit: " + exit;
    }
}