#!/usr/bin/env python3

import argparse
import sys
import os
import csv
import re
from struct import pack, pack_into, calcsize
import time
import datetime
import mmap

class Spinner:
    def __init__(self, step):
        self._n = self._x = 0
        self._chars = '\\|/-'
        self._step = step

    def spin(self):
        self._n += 1

        if self._n % self._step == 0:
            sys.stdout.write('\b' + self._chars[self._x % 4])
            sys.stdout.flush()

            self._x += 1

            if self._x >= 4:
                self._x = 0

            self._n = 0

spinner = Spinner(100000)

class Input:
    def __init__(self, path):
        if args.verbose:
            print('[INFO] Trying to read data from %s...' % path)
        try:
            self.path = open(path, 'r')
        except OSError as e:
            print("[ERROR] '%s' raised when tried to read the file '%s'" % (e.strerror, e.filename))
            sys.exit(1)

    def __del__(self):
        self.path.close()

    def _addBar(self, barTimestamp, tickTimestamp, open, high, low, close, volume):
        self.uniBars += [{
            'barTimestamp': barTimestamp,
           'tickTimestamp': tickTimestamp,
                    'open': open,
                    'high': high,
                     'low': low,
                   'close': close,
                  'volume': volume
        }]

def string_to_timestamp(s):
    try_microseconds = s[20:]

    if try_microseconds == '':
        microseconds = int(s[20:])
    else:
        microseconds = 0

    return datetime.datetime(
            int(s[0:4]),   # Year
            int(s[5:7]),   # Month
            int(s[8:10]),  # Day
            int(s[11:13]), # Hour
            int(s[14:16]), # Minute
            int(s[17:19]), # Second
            microseconds,  # Microseconds
            datetime.timezone.utc)

class CSV(Input):
    def __init__(self, path):
        super().__init__(path)
        if os.name == "nt":
            self._map_obj = mmap.mmap(self.path.fileno(), 0, access=mmap.ACCESS_READ)
        else:
            self._map_obj = mmap.mmap(self.path.fileno(), 0, prot=mmap.PROT_READ)

    def __iter__(self):
        return self

    def __next__(self):
        line = self._map_obj.readline()
        if line:
            isLastRow = self._map_obj.tell () == self._map_obj.size ()
            return (self._parseLine(line), isLastRow)
        else:
            raise StopIteration

    def _parseLine(self, line):
        tick = line.split(b',')
        return {
            # Storing timestamp as float to preserve its precision.
            # 'timestamp': time.mktime(datetime.datetime.strptime(tick[0], '%Y.%m.%d %H:%M:%S.%f').replace(tzinfo=datetime.timezone.utc).timetuple()),
            'timestamp': string_to_timestamp(tick[0]).timestamp(),
             'bidPrice': float(tick[1]),
             'askPrice': float(tick[2]),
            'bidVolume': float(tick[3]),
            'askVolume': float(tick[4])               # float() handles ending '\n' character
        }

class Output:
    def __init__(self, timeframe, path_suffix, symbol, output_dir):
        self.deltaTimestamp = timeframe * 60
        self.endTimestamp = None
        self.barCount = 0

        self.filename = '%s%d%s' % (symbol, timeframe, path_suffix)
        self.fullname = os.path.join(output_dir, self.filename)

        try:
            os.remove(self.fullname)  # Remove existing output file before creating an appended new one
        except (OSError, IOError) as e:
            pass
        try:
            self.path = open(self.fullname, 'wb')
        except OSError as e:
            print("[ERROR] '%s' raised when tried to open for appending the file '%s'" % (e.strerror, e.filename))
            sys.exit(1)


    def __del__(self):
        self.path.close()

    def finalize(self):
        pass

    def _aggregate(self, tick):
        if not self.endTimestamp or tick['timestamp'] >= self.endTimestamp:
            uniBar = None
            if self.endTimestamp: uniBar = {
                'barTimestamp': self.startTimestamp,
               'tickTimestamp': tick['timestamp'],
                        'open': self.open,
                        'high': self.high,
                         'low': self.low,
                       'close': self.close,
                      'volume': self.volume
            }

            self.startTimestamp = (int(tick['timestamp']) // self.deltaTimestamp) * self.deltaTimestamp
            self.endTimestamp = self.startTimestamp + self.deltaTimestamp
            self.open = self.high = self.low = self.close = tick['bidPrice']
            self.volume = tick['bidVolume'] + tick['askVolume']

            if uniBar: return (uniBar, True)
        else:
            self.high = max(tick['bidPrice'], self.high)
            self.low  = min(tick['bidPrice'], self.low)
            self.close = tick['bidPrice']
            self.volume += tick['bidVolume'] + tick['askVolume']

        uniBar = {
            'barTimestamp': self.startTimestamp,
           'tickTimestamp': tick['timestamp'],
                    'open': self.open,
                    'high': self.high,
                     'low': self.low,
                   'close': self.close,
                  'volume': self.volume
        }
        return (uniBar, False)


    def _aggregateWithTicks(self, tick):
        if not self.endTimestamp or tick['timestamp'] >= self.endTimestamp:
            self.startTimestamp = (int(tick['timestamp']) // self.deltaTimestamp) * self.deltaTimestamp
            self.endTimestamp = self.startTimestamp + self.deltaTimestamp
            self.open = self.high = self.low = tick['bidPrice']
            self.volume = tick['bidVolume'] + tick['askVolume']
            self.barCount += 1
        else:
            self.high = max(tick['bidPrice'], self.high)
            self.low  = min(tick['bidPrice'], self.low)
            self.volume += tick['bidVolume'] + tick['askVolume']

        return {
            'barTimestamp': self.startTimestamp,
           'tickTimestamp': tick['timestamp'],
                    'open': self.open,
                    'high': self.high,
                     'low': self.low,
                   'close': tick['bidPrice'],
                  'volume': self.volume
        }


class HST509(Output):
    def __init__(self, path, path_suffix, output_dir, timeframe, symbol):
        # Initialize variables in parent constructor
        super().__init__(timeframe, path_suffix, symbol, output_dir)

        # Build header (148 Bytes in total)
        header = bytearray()
        header += pack('<i', 400)                                                    # Version
        header += bytearray('(C)opyright 2003, MetaQuotes Software Corp.'.ljust(64,  # Copyright
                            '\x00'),'latin1', 'ignore')
        header += bytearray(symbol.ljust(12, '\x00'), 'latin1', 'ignore')            # Symbol
        header += pack('<i', timeframe)                                              # Period
        header += pack('<i', 5)                                                      # Digits, using the default value of HST format
        header += pack('<i', int(time.time()))                                       # Time of sign (database creation)
        header += pack('<i', 0)                                                      # Time of last synchronization
        header += bytearray(13*4)                                                    # Space for future use

        self.path.write(header)

    def pack_ticks(self, tick):
        # Transform universal bar list to binary bar data (44 Bytes per bar)
        (uniBar, newUniBar) = self._aggregate(tick)
        if newUniBar:
            self.path.write(self._packUniBar(uniBar))

    def _packUniBar(self, uniBar):
        bar = bytearray()
        bar += pack('<i', uniBar['barTimestamp'])      # Time
        bar += pack('<d', uniBar['open'])              # Open
        bar += pack('<d', uniBar['low'])               # Low
        bar += pack('<d', uniBar['high'])              # High
        bar += pack('<d', uniBar['close'])             # Close
        bar += pack('<d', max(uniBar['volume'], 1.0))  # Volume

        return bar


class HST574(Output):
    def __init__(self, path, path_suffix, output_dir, timeframe, symbol):
        # Initialize variables in parent constructor
        super().__init__(timeframe, path_suffix, symbol, output_dir)

        # Build header (148 Bytes in total)
        header = bytearray()
        header += pack('<i', 401)                                                    # Version
        header += bytearray('(C)opyright 2003, MetaQuotes Software Corp.'.ljust(64,  # Copyright
                            '\x00'),'latin1', 'ignore')
        header += bytearray(symbol.ljust(12, '\x00'), 'latin1', 'ignore')            # Symbol
        header += pack('<i', timeframe)                                              # Period
        header += pack('<i', 5)                                                      # Digits, using the default value of HST format
        header += pack('<i', int(time.time()))                                       # Time of sign (database creation)
        header += pack('<i', 0)                                                      # Time of last synchronization
        header += bytearray(13*4)                                                    # Space for future use

        self.path.write(header)

    def pack_ticks(self, ticks):
        # Transform universal bar list to binary bar data (60 Bytes per bar)
        ticksAggregated  = {
            'barTimestamp':  ticks[0]['timestamp'],
            'tickTimestamp': ticks[0]['timestamp'],
            'open':          ticks[0]['bidPrice'],
            'low':           ticks[0]['bidPrice'],
            'high':          ticks[0]['bidPrice'],
            'close':         ticks[0]['bidPrice'],
            'volume':        0
        }

        for tick in ticks:
            ticksAggregated['low']     = min(ticksAggregated['low'],  tick['bidPrice'])
            ticksAggregated['high']    = max(ticksAggregated['high'], tick['bidPrice'])
            ticksAggregated['volume'] += tick['bidVolume'] + tick['askVolume']

        ticksAggregated['close'] = tick['bidPrice']

        self.path.write(self._packUniBar(ticksAggregated))

    def _packUniBar(self, uniBar):
        bar = bytearray()
        bar += pack('<i', uniBar['barTimestamp'])           # Time
        bar += bytearray(4)                                 # Add 4 bytes of padding.
        # OHLCV values.
        bar += pack('<d', uniBar['open'])                   # Open
        bar += pack('<d', uniBar['high'])                   # High
        bar += pack('<d', uniBar['low'])                    # Low
        bar += pack('<d', uniBar['close'])                  # Close
        bar += pack('<Q', max(int(uniBar['volume']), 1))    # Volume
        bar += pack('<i', 0)                                # Spread
        bar += pack('<Q', 0)                                # Real volume

        return bar

class FXT(Output):
    def __init__(self, path, path_suffix, output_dir, timeframe, symbol, server, spread, model):
        # Initialize variables in parent constructor
        super().__init__(timeframe, path_suffix, symbol, output_dir)

        self._priv = (timeframe, server, symbol, spread, model)
        self._firstUniBar = self._lastUniBar = None

        # Build header (728 Bytes in total).
        header = bytearray()
        header += pack('<I', 405)                                                       # FXT header version: 405
        header += bytearray('Copyright 2001-2015, MetaQuotes Software Corp.'.ljust(64,  # Copyright text.
                            '\x00'), 'latin1', 'ignore')
        header += bytearray(server.ljust(128, '\x00'), 'latin1', 'ignore')              # Account server name.
        header += bytearray(symbol.ljust(12, '\x00'), 'latin1', 'ignore')               # Symbol pair.
        header += pack('<I', timeframe)                                                 # Period of data aggregation in minutes (timeframe).
        header += pack('<I', model)                                                     # Model type: 0 - every tick, 1 - control points, 2 - bar open.
        header += pack('<I', 0)                                                         # Bars - amount of bars in history.
        header += pack('<I', 0)                                                         # Modelling start date - date of the first tick.
        header += pack('<I', 0)                                                         # Modelling end date - date of the last tick.
        header += bytearray(4)                                                          # Add 4 bytes of padding. This potentially can be totalTicks.
        header += pack('<d', 99.9)                                                      # Modeling quality (max. 99.9).
        # General parameters.
        header += bytearray('EUR'.ljust(12, '\x00'), 'latin1', 'ignore')                # Base currency (12 bytes).
        header += pack('<I', spread)                                                    # Spread in points.
        header += pack('<I', 5)                                                         # Digits, using the default value of FXT format.
        header += bytearray(4)                                                          # Add 4 bytes of padding.
        header += pack('<d', 1e-5)                                                      # Point size (e.g. 0.00001).
        header += pack('<I', 1)                                                         # Minimal lot size in centi lots (hundredths).
        header += pack('<I', 50000)                                                     # Maximal lot size in centi lots (hundredths).
        header += pack('<I', 1)                                                         # Lot step in centi lots (hundredths).
        header += pack('<I', 10)                                                        # Stops level value (orders stop distance in points).
        header += pack('<I', 1)                                                         # GTC (Good till cancel) - instruction to close pending orders at end of day (default: True).
        header += bytearray(4)                                                          # Add 4 bytes of padding.
        # Profit Calculation parameters.
        header += pack('<d', 100000.0)                                                  # ContractSize - contract size
        header += pack('<d', 0.0)                                                       # Tick value in quote currency (empty).
        header += pack('<d', 0.0)                                                       # Size of one tick (empty).
        header += pack('<I', 0)                                                         # Profit calculation mode: 0 - Forex, 1 - CFD, 2 - Futures.
        # Swap calculation
        header += pack('<i', 0)                                                         # Enable swap (default: False).
        header += pack('<I', 0)                                                         # Swap calculation method: 0 - in points, 1 - in the symbol base currency, 2 - by interest, 3 - in the margin currency.
        header += bytearray(4)                                                          # Add 4 bytes of padding.
        header += pack('<d', 0.0)                                                       # Swap of the buy order - long overnight swap value.
        header += pack('<d', 0.0)                                                       # Swap of the sell order - short overnight swap value.
        header += pack('<I', 3)                                                         # Day of week to charge 3 days swap rollover. Default: WEDNESDAY (3).
        # Margin calculation.
        header += pack('<I', 100)                                                       # Account leverage (default: 100).
        header += pack('<I', 1)                                                         # Free margin calculation mode {MARGIN_DONT_USE, MARGIN_USE_ALL, MARGIN_USE_PROFIT, MARGIN_USE_LOSS}
        header += pack('<I', 0)                                                         # Margin calculation mode: 0 - Forex, 1 - CFD, 2 - Futures, 3 - CFD for indexes.
        header += pack('<I', 30)                                                        # Margin stopout level (default: 30).
        header += pack('<I', 0)                                                         # Margin stop out check mode {MARGIN_TYPE_PERCENT, MARGIN_TYPE_CURRENCY}
        header += pack('<d', 0.0)                                                       # Margin requirements.
        header += pack('<d', 0.0)                                                       # Margin maintenance requirements.
        header += pack('<d', 50000.0)                                                   # Margin requirements for hedged positions.
        header += pack('<d', 1.25)                                                      # Margin divider used for leverage calculation.
        header += bytearray('USD'.ljust(12, '\x00'), 'latin1', 'ignore')                # Margin currency.
        header += bytearray(4)                                                          # Padding space - add 4 bytes to align the next double.
        # Commission calculation.
        header += pack('<d', 0.0)                                                       # Basic commission.
        header += pack('<i', 1)                                                         # Basic commission type {COMM_TYPE_MONEY, COMM_TYPE_PIPS, COMM_TYPE_PERCENT}.
        header += pack('<i', 0)                                                         # Commission per lot or per deal {COMMISSION_PER_LOT, COMMISSION_PER_DEAL}.
        # For internal use.
        header += pack('<I', 1)                                                         # Index of the first bar at which modeling started (0 for the first bar).
        header += pack('<I', 0)                                                         # Index of the last bar at which modeling started (0 for the last bar).
        header += pack('<I', 0)                                                         # Bar index where modeling started using M1 bars (0 for the first bar).
        header += pack('<I', 0)                                                         # Bar index where modeling started using M5 bars (0 for the first bar).
        header += pack('<I', 0)                                                         # Bar index where modeling started using M15 bars (0 for the first bar).
        header += pack('<I', 0)                                                         # Bar index where modeling started using M30 bars (0 for the first bar).
        header += pack('<I', 0)                                                         # Bar index where modeling started using H1 bars (0 for the first bar).
        header += pack('<I', 0)                                                         # Bar index where modeling started using H4 bars (0 for the first bar).
        header += pack('<I', 0)                                                         # Begin date from tester settings (must be zero).
        header += pack('<I', 0)                                                         # End date from tester settings (must be zero).
        header += pack('<I', 0)                                                         # Order's freeze level in points.
        header += pack('<I', 0)                                                         # Number of errors during model generation which needs to be fixed before testing.
        header += bytearray(60*4)                                                       # Reserved - Space for future use.

        self.path.write(header)

    def write_unibar(self, tick):
        # We're getting an array
        uniBar = {
            'barTimestamp': tick['barTimestamp'],
           'tickTimestamp': tick['timestamp'],
                    'open': tick['bidPrice'],
                    'high': tick['bidPrice'],
                     'low': tick['bidPrice'],
                   'close': tick['bidPrice'],
                  'volume': tick['bidVolume']
        }
        if not self._firstUniBar: self._firstUniBar = uniBar             # Store first and ...
        self._lastUniBar = uniBar                                        # ... last bar data for header.
        self.path.write(pack('<iiddddQii',
                int(uniBar['barTimestamp']),                                 # Bar datetime.
                0,                                                           # Add 4 bytes of padding.
                uniBar['open'],uniBar['high'],uniBar['low'],uniBar['close'], # OHLCV values.
                max(int(uniBar['volume']), 1),                               # Volume (documentation says it's a double, though it's stored as a long int).
                int(uniBar['tickTimestamp']),                                # The current time within a bar.
                4))                                                          # Flag to launch an expert (0 - bar will be modified, but the expert will not be launched).

    def pack_ticks(self, ticks):
        # Transform universal bar list to binary bar data (56 Bytes per bar)
        model = self._priv[4]

        # Every tick model
        if model == 0:
            for tick in ticks:
                self.write_unibar(tick)
        # Control points model
        elif model == 1:
            startTimestamp = None
            self.write_unibar(ticks[0])
            lowPrice = highPrice = ticks[0]['bidPrice']
            for tick in ticks[1:]:
                # Beginning of the M1 bar's timeline.
                tick['barTimestamp'] = int(tick['timestamp']) - int(tick['timestamp']) % 60

                if not startTimestamp:
                    startTimestamp = tick['barTimestamp']

                # Determines the end of the M1 bar.
                endTimestampTimeline  = startTimestamp + 60

                if tick['bidPrice'] < lowPrice:
                    lowPrice = tick['bidPrice']
                    self.write_unibar(tick)
                elif tick['bidPrice'] > highPrice:
                    highPrice = tick['bidPrice']
                    self.write_unibar(tick)
                elif tick['timestamp'] >= endTimestampTimeline:
                    startTimestamp   = tick['barTimestamp']
                    self.write_unibar(tick)
        # Open price model
        elif model == 2:
            self.write_unibar(ticks[0])

    def finalize(self):
        # Fixup the header.
        self.path.seek(216)
        fix  = bytearray()
        fix += pack('<III',
                self.barCount,
                int(self._firstUniBar['barTimestamp']),                    # Modelling start date - date of the first tick.
                int(self._lastUniBar['barTimestamp']))                     # Modelling end date - date of the last tick.
        self.path.write(fix)

        self.path.seek(472)
        fix  = bytearray()
        fix += pack('<II',
                int(self._firstUniBar['barTimestamp']),                    # Tester start date - date of the first tick.
                int(self._lastUniBar['barTimestamp']))                     # Tester end date - date of the last tick.
        self.path.write(fix)


class HCC(Output):
    """Output ticks in HCC file format."""

    def __init__(self, path_suffix, output_dir, timeframe, symbol):
        """Create file and write headers."""

        super().__init__(timeframe, path_suffix, symbol, output_dir)

        # Build header (228 Bytes in total)
        header = bytearray()
        header += pack('<I', 501)                                                      # Magic
        header += bytearray('Copyright 2001-2016, MetaQuotes Software Corp.'.ljust(64, # Copyright
                            '\x00'),'utf-16', 'ignore')[2:]
        header += bytearray('History'.ljust(16, '\x00'), 'utf-16', 'ignore')[2:]       # Name
        header += bytearray('EURUSD'.ljust(32, '\x00'), 'utf-16', 'ignore')[2:]        # Title
        assert 228 == self.path.write(header)

        # Build EMPTY table (18 Bytes in total)
        table = bytearray()
        table += pack('<i', 0) # unknown_0
        table += pack('<i', 0) # unknown_1
        table += pack('<h', 0) # unknown_2
        table += pack('<i', 0) # size
        table += pack('<i', 0) # off

        # write main table (18 Bytes in total)
        assert 18 == self.path.write(table)
        self.table_end = self.path.tell()
        # write an empty table record to indicate that there are no more tables
        assert 18 == self.path.write(table)

        # Build record header (189 Bytes in total)
        record_header = bytearray()
        record_header += pack('<H',0x81)                                               # magic
        record_header += bytearray('LABEL'.ljust(32, '\x00'), 'utf-16', 'ignore')[2:]  # label
        record_header += bytearray('UN0'.ljust(9, '\x00'), 'utf-16', 'ignore')[2:]     # unknown_0
        record_header += pack('<I', 0)                                                 # rows
        record_header += bytearray('UN1'.ljust(50, '\x00'), 'utf-16', 'ignore')[2:]    # unknown_1
        record_header += pack('<c', b'0')
        self.record_header_begin = self.path.tell()
        assert 189 == self.path.write(record_header)
        self.record_header_end = self.path.tell()

    def pack_ticks(self, ticks):
        """Prepare and write ticks in file."""

        self.count_ticks = len(ticks)

        # Transform universal bar list to binary bar data (40 Bytes per bar)
        for tick in ticks:
            # We're getting an array
            uniBar = {
                'barTimestamp': tick['barTimestamp'],
               'tickTimestamp': tick['timestamp'],
                        'open': tick['bidPrice'],
                        'high': tick['bidPrice'],
                         'low': tick['bidPrice'],
                       'close': tick['bidPrice'],
                      'volume': tick['bidVolume']
            }

            self.path.write(pack('<iidddd',
                    0x00088884,                                                   # Separator
                    int(uniBar['barTimestamp']),                                  # Bar datetime.
                    uniBar['open'],uniBar['high'],uniBar['low'],uniBar['close'])) # Values.

    def finalize(self):
        """Write data count in headers."""

        # fixup the table
        fix = pack('<i', self.record_header_begin)
        self.path.seek(self.table_end-4)
        self.path.write(fix)

        # fixup the record header
        fix = pack('<I', self.count_ticks)
        self.path.seek(self.record_header_end-101-4)
        self.path.write(fix)


def config_argparser():
    argumentParser = argparse.ArgumentParser(add_help=False)
    argumentParser.add_argument('-i', '--input-file',
        action='store',      dest='inputFile', help='input file', default=None, required=True)
    argumentParser.add_argument('-f', '--output-format',
        action='store',      dest='outputFormat', help='format of output file (FXT/HST/Old HST/HCC), as: fxt4/hst4/hst4_509/hcc', default='fxt4')
    argumentParser.add_argument('-s', '--symbol',
        action='store',      dest='symbol', help='symbol code (maximum 12 characters)', default='EURUSD')
    argumentParser.add_argument('-t', '--timeframe',
        action='store',      dest='timeframe', help='one of the timeframe values: M1, M5, M15, M30, H1, H4, D1, W1, MN', default='M1')
    argumentParser.add_argument('-p', '--spread',
        action='store',      dest='spread', help='spread value in points', default=20)
    argumentParser.add_argument('-d', '--output-dir',
        action='store',      dest='outputDir', help='destination directory to save the output file', default='.')
    argumentParser.add_argument('-S', '--server',
        action='store',      dest='server', help='name of FX server', default='default')
    argumentParser.add_argument('-v', '--verbose',
        action='store_true', dest='verbose', help='increase output verbosity')
    argumentParser.add_argument('-m', '--model',
        action='store',      dest='model', help='one of the model values: 0, 1, 2', default='0')
    argumentParser.add_argument('-h', '--help',
        action='help', help='Show this help message and exit')

    return argumentParser


def construct_queue(timeframe_list):
    """Select the apropriate classes and begin the work."""

    for timeframe in timeframe_list:
        if multiple_timeframes:
            print('[INFO] Queueing the {}m timeframe for conversion'.format(timeframe))
        # Checking output file format argument and doing conversion
        if outputFormat == 'hst4_509':
            yield HST509(None, '.hst', args.outputDir, timeframe, symbol)
        elif outputFormat == 'hst4':
            yield HST574(None, '.hst', args.outputDir, timeframe, symbol)
        elif outputFormat == 'fxt4':
            for m in args.model.split(','):
                yield FXT(None, '_{0}.fxt'.format(m), args.outputDir, timeframe, symbol, server, spread, int(m))
        elif outputFormat == 'hcc':
            yield HCC('.hcc', args.outputDir, timeframe, symbol)
        else:
            print('[ERROR] Unknown output file format!')
            sys.exit(1)


def process_queue(queue):
    """Process the queue, process all the timeframes at the same time to amortize the cost of the parsing."""

    queue = list(queue)

    try:
        for obj in queue:

            ticks = CSV(args.inputFile);

            startTimestamp = None

            # We will retrieve all ticks in the timeframe into following array and update their LowBid/HighBid
            ticksToJoin      = [];
            ticksToAggregate = [];

            for (tick, isLastRow) in ticks:
                # Beginning of the bar's timeline.
                tick['barTimestamp'] = int(tick['timestamp']) - int(tick['timestamp']) % obj.deltaTimestamp

                # Tick's timestamp will be rounded to 1 for M1 and 60 for other.
                tick['timestamp'] = int(tick['timestamp']) #- int(tick['timestamp']) % (1 if obj.deltaTimestamp == 60 else 60)

                if not startTimestamp:
                    startTimestamp = tick['barTimestamp']

                # Tick after this time won't be used for LowBid/HighBid aggregation.
                endTimestampAggregate = startTimestamp + 60

                # Determines the end of the current bar.
                endTimestampTimeline  = startTimestamp + obj.deltaTimestamp

                if tick['timestamp'] < endTimestampTimeline:
                    # Tick is within the current bar's timeline, queuing for
                    # aggregation.
                    ticksToAggregate.append(tick)
                else:
                    # Tick is beyond current bar's timeline, aggregating unaggregated
                    # ticks:
                    if len(ticksToAggregate) > 0:
                        obj.pack_ticks(ticksToAggregate)

                    # Next bar's timeline will begin from this new tick's bar
                    # timestamp.
                    startTimestamp   = tick['barTimestamp']

                    # Tick beyond delta timeframe will be aggregated in the next
                    # timeframe
                    ticksToAggregate = [tick]

                spinner.spin()

            # Writting the last tick if not yet written.
            if len(ticksToAggregate) > 0:
                obj.pack_ticks(ticksToAggregate)

        if args.verbose:
            print('[INFO] Finalizing...')
        for obj in queue:
            obj.finalize()
        if args.verbose:
            print('[INFO] Done.')
    except KeyboardInterrupt as e:
        print('\n[INFO] Exiting by user request...')
        sys.exit()


if __name__ == '__main__':
    # Parse the arguments
    arg_parser = config_argparser()
    args = arg_parser.parse_args()

    # Checking input file argument
    if args.verbose:
        print('[INFO] Input file: %s' % args.inputFile)

    # Checking symbol argument
    if len(args.symbol) > 12:
        print('[WARNING] Symbol is more than 12 characters, cutting its end off!')
        symbol = args.symbol[0:12]
    else:
        symbol = args.symbol
    if args.verbose:
        print('[INFO] Symbol name: %s' % symbol)

    # Converting timeframe argument to minutes
    timeframe_list = []



    timeframe_conv = {
        'M': 1,
        'H': 60,
        'D': 24 * 60,
        'W': 7 * 24 * 60,
        'MN': 30 * 24 * 60
    }

    for arg in args.timeframe.lower().split(','):
        match_obj = re.match(r'(M|H|D|W|MN)(\d+)', arg, re.I)
        if match_obj:
            model = match_obj.group(1).upper()
            value = int(match_obj.group(2))

            timeframe_list.append(
                timeframe_conv[model] * value
            )
        else:
            print('[ERROR] Bad timeframe setting \'{}\'!'.format(arg))
            sys.exit(1)

    if args.verbose:
        print('[INFO] Timeframe: %s - %s minute(s)' % (args.timeframe.upper(), timeframe_list))

    # Checking spread argument
    spread = int(args.spread)
    if args.verbose:
        print('[INFO] Spread: %d' % spread)

    # Create output directory
    os.makedirs(args.outputDir, 0o755, True)
    if args.verbose:
        print('[INFO] Output directory: %s' % args.outputDir)

    # Checking server argument
    if len(args.server) > 128:
        print('[WARNING] Server name is longer than 128 characters, cutting its end off!')
        server = args.server[0:128]
    else:
        server = args.server
    if args.verbose:
        print('[INFO] Server name: %s' % server)

    outputFormat = args.outputFormat.lower()
    if args.verbose:
        print('[INFO] Output format: %s' % outputFormat)

    multiple_timeframes = len(timeframe_list) > 1

    queue = construct_queue(timeframe_list)
    process_queue(queue)