Repository: Ma-r-co/savoia

Issue:
Tear off output_results from Portfolio, and create Result Handler.
Tear off output_results from Portfolio, and create Result Handler.

Discussion:

Patch:
diff --git a/src/savoia/engine/engine.py b/src/savoia/engine/engine.py
index 6f7c231..9dca2a4 100644
--- a/src/savoia/engine/engine.py
+++ b/src/savoia/engine/engine.py
@@ -5,6 +5,7 @@ from savoia.ticker.ticker import Ticker
 from savoia.strategy.strategy import Strategy
 from savoia.portfolio.portfolio import Portfolio
 from savoia.execution.execution import ExecutionHandler
+from savoia.result.result import Result, ResultHandler
 from savoia.config.decimal_config import initializeDecimalContext
 
 from logging import getLogger, Logger
@@ -34,6 +35,12 @@ class strategy_params(TypedDict):
         'Queue[Event]']]
 
 
+class result_params(TypedDict):
+    module_name: str
+    params: Dict[str, Union[str, Decimal, int, float, List[Pair],
+        'Queue[Result]']]
+
+
 class engine_params(TypedDict):
     pairs: List[Pair]
     home_currency: str
@@ -53,6 +60,7 @@ class Engine(object):
     datafeed: DataFeeder
     execution: ExecutionHandler
     strategy: Strategy
+    result: ResultHandler
     portfolio: Portfolio
     ticker: Ticker
     equity: Decimal
@@ -63,10 +71,12 @@ class Engine(object):
     event_q: 'Queue[Event]'
     feed_q: 'Queue[Event]'
     exec_q: 'Queue[Event]'
+    result_q: 'Queue[Result]'
 
     def __init__(
         self, engine: engine_params, datafeed: datafeed_params,
-        execution: execution_params, strategy: strategy_params
+        execution: execution_params, strategy: strategy_params,
+        result: result_params
     ):
         """
         Initializes the backtest.
@@ -81,18 +91,20 @@ class Engine(object):
         self.event_q = Queue()
         self.feed_q = Queue()
         self.exec_q = Queue()
+        self.result_q = Queue()
         self.iters = 0
         self.datafeed = self._setup_datafeed(datafeed)
         self.execution = self._setup_execution(execution)
         self.strategy = self._setup_strategy(strategy)
+        self.result = self._setup_result(result)
         self.ticker = Ticker(self.pairs)
         self.portfolio = Portfolio(
             ticker=self.ticker,
             event_q=self.event_q,
+            result_q=self.result_q,
             home_currency=self.home_currency,
             pairs=self.pairs,
-            equity=self.equity,
-            isBacktest=self.isBacktest
+            equity=self.equity
         )
         self.toContinue = True
         initializeDecimalContext()
@@ -129,6 +141,16 @@ class Engine(object):
         exe = getattr(_module, strategy['module_name'])
         return exe(**_params)
 
+    def _setup_result(self, result: result_params) -> ResultHandler:
+        _module = import_module('savoia.result.result')
+        
+        _params = result['params']
+        _params['pairs'] = self.pairs
+        _params['result_q'] = self.result_q
+
+        exe = getattr(_module, result['module_name'])
+        return exe(**_params)
+
     def _run_engine(self) -> None:
         """
         Carries out an infinite while loop that polls the
@@ -187,10 +209,12 @@ class Engine(object):
         self.portfolio.output_results()
 
     def _run(self) -> None:
+        _result = threading.Thread(target=self.result.run)
         _datafeed = threading.Thread(target=self.datafeed.run)
         _execution = threading.Thread(target=self.execution.run)
         _engine = threading.Thread(target=self._run_engine)
 
+        _result.start()
         _execution.start()
         _engine.start()
         _datafeed.start()
@@ -198,6 +222,8 @@ class Engine(object):
         _datafeed.join()
         _engine.join()
         _execution.join()
+        self.result_q.put(None)
+        _result.join()
 
     def run(self) -> None:
         """
@@ -207,7 +233,7 @@ class Engine(object):
             _start = time.time()
             self.logger.info('Start Backtesting.')
             self._run()
-            self._output_performance()
+            # self._output_performance()
             self.logger.info("Backtest complete. Elapsed Time[Sec]: " +
                 f'{time.time() - _start}')
         else:
diff --git a/src/savoia/portfolio/portfolio.py b/src/savoia/portfolio/portfolio.py
index 3db5754..0c051b1 100644
--- a/src/savoia/portfolio/portfolio.py
+++ b/src/savoia/portfolio/portfolio.py
@@ -1,49 +1,44 @@
 from decimal import Decimal
 from queue import Queue
-import os
-
-import pandas as pd
 
 from savoia.ticker.ticker import Ticker
 from savoia.event.event import OrderEvent, Event, TickEvent, \
     SignalEvent, FillEvent
-from savoia.performance.performance import create_drawdowns
 from savoia.portfolio.position import Position
-from savoia.config.dir_config import OUTPUT_RESULTS_DIR
 from savoia.types.types import Pair
+from savoia.result.result import Result, EquityResult, ExecutionResult
 
 from logging import getLogger, Logger
-from typing import Dict, TextIO, List
+from typing import Dict, List
 
 
 class Portfolio(object):
     logger: Logger
     ticker: Ticker
     event_q: 'Queue[Event]'
+    result_q: 'Queue[Result]'
     home_currency: str
     equity: Decimal
     balance: Decimal
     upl: Decimal
-    isBacktest: bool
     pairs: List[Pair]
     positions: Dict[Pair, Position]
 
     def __init__(
-        self, ticker: Ticker, event_q: 'Queue[Event]', home_currency: str,
-        pairs: List[Pair], equity: Decimal, isBacktest: bool = True
+        self, ticker: Ticker, event_q: 'Queue[Event]',
+        result_q: 'Queue[Result]', home_currency: str, pairs: List[Pair],
+        equity: Decimal
     ):
         self.logger = getLogger(__name__)
         self.ticker = ticker
         self.event_q = event_q
+        self.result_q = result_q
         self.home_currency = home_currency
         self.equity = equity
         self.balance = self.equity
         self.upl = Decimal('0')
         self.pairs = pairs
-        self.isBacktest = isBacktest
         self.positions = self._initialize_positions()
-        if self.isBacktest:
-            self.backtest_file = self._create_equity_file()
 
     def _initialize_positions(self) -> Dict[Pair, Position]:
         _pos = {}
@@ -51,60 +46,26 @@ class Portfolio(object):
             _pos[_pair] = Position(self.home_currency, _pair, self.ticker)
         return _pos
 
-    def _create_equity_file(self) -> TextIO:
-        filename: str = "backtest.csv"
-        out_file: TextIO = open(os.path.join(OUTPUT_RESULTS_DIR, filename), "w")
-        header: str = "Timestamp,Equity"
-        for pair in self.ticker.pairs:
-            header += ",%s" % pair
-        header += "\n"
-        out_file.write(header)
-        if self.isBacktest:
-            self.logger.info("Created equity file. Header as: %s", header[:-1])
-        return out_file
-
-    def output_results(self) -> None:
-        # Closes off the Backtest.csv file so it can be
-        # read via Pandas without problems
-        self.backtest_file.close()
-
-        in_filename = "backtest.csv"
-        out_filename = "equity.csv"
-        in_file = os.path.join(OUTPUT_RESULTS_DIR, in_filename)
-        out_file = os.path.join(OUTPUT_RESULTS_DIR, out_filename)
-
-        # Create equity curve dataframe
-        df = pd.read_csv(in_file, index_col=0)
-        df.dropna(inplace=True)
-        df["Total"] = df.sum(axis=1)
-        df["Returns"] = df["Total"].pct_change()
-        df["Equity"] = (1.0 + df["Returns"]).cumprod()
-
-        # Create drawdown statistics
-        drawdown, max_dd, dd_duration = create_drawdowns(df["Equity"])
-        df["Drawdown"] = drawdown
-        df.to_csv(out_file, index=True)
-
-        self.logger.info("Simulation complete and results exported to %s",
-            out_filename)
-
     def update_portfolio(self, event: TickEvent) -> None:
         """
         This updates all positions ensuring an up to date
         unrealised profit and loss (upl).
         """
-        _upl: Decimal = Decimal('0')
+        _upl: Dict[str, Decimal] = {}
+        _result: EquityResult
 
-        for _pos in self.positions.values():
-            _upl += _pos.update_position_price()
-        self.upl = _upl
+        for _pair in self.pairs:
+            _upl[_pair] = self.positions[_pair].update_position_price()
+        self.upl = Decimal(str(sum(_upl.values())))
         self._update_equity()
-        if self.isBacktest:
-            out_line = f'{event.time}, {self.equity}'
-            for pair in self.ticker.pairs:
-                out_line += f",{self.positions[pair].upl}"
-            out_line += "\n"
-            self.backtest_file.write(out_line)
+        _upl['total'] = self.upl
+        _result = EquityResult(
+            time=event.time,
+            equity=self.equity,
+            balance=self.balance,
+            upl=_upl
+        )
+        self.result_q.put(_result)
 
     def execute_signal(self, event: SignalEvent) -> None:
         '''Handles SignalEvent'''
@@ -124,7 +85,7 @@ class Portfolio(object):
             self.event_q.put(order)
             self.logger.info(f"OrderEvent Issued: {order}")
         else:
-            self.logger.info(
+            self.logger.error(
                 "Unable to execute order as price data was insufficient."
             )
 
@@ -140,6 +101,14 @@ class Portfolio(object):
 
         self._update_equity(_delta_balance, _delta_upl)
 
+        _result = ExecutionResult(
+            time=event.time,
+            pair=event.pair,
+            units=event.units,
+            price=event.price
+        )
+        self.result_q.put(_result)
+
     def _update_equity(self, delta_balance: Decimal = Decimal('0'),
             delta_upl: Decimal = Decimal('0')) -> None:
         self.balance += delta_balance
diff --git a/src/savoia/result/result.py b/src/savoia/result/result.py
new file mode 100644
index 0000000..edfe886
--- /dev/null
+++ b/src/savoia/result/result.py
@@ -0,0 +1,161 @@
+from abc import ABCMeta, abstractmethod
+from logging import Logger, getLogger
+import pandas as pd
+import os
+from queue import Queue, Empty
+from decimal import Decimal
+
+from typing import List, TextIO, Dict
+
+from savoia.types.types import Pair
+
+
+class Result(metaclass=ABCMeta):
+    type: str
+    time: pd.Timestamp
+
+
+class EquityResult(Result):
+    equity: Decimal
+    balance: Decimal
+    upl: Dict[str, Decimal]
+
+    def __init__(self,
+            time: pd.Timestamp,
+            equity: Decimal,
+            balance: Decimal,
+            upl: Dict[str, Decimal]):
+        self.type = 'EquityResult'
+        self.time = time
+        self.equity = equity
+        self.balance = balance
+        self.upl = upl
+
+
+class ExecutionResult(Result):
+    pair: Pair
+    units: Decimal
+    price: Decimal
+
+    def __init__(self,
+            time: pd.Timestamp,
+            pair: Pair,
+            units: Decimal,
+            price: Decimal):
+        self.type = 'ExecutionResult'
+        self.time = time
+        self.pair = pair
+        self.units = units
+        self.price = price
+
+
+class ResultHandler(metaclass=ABCMeta):
+    logger: Logger
+    pairs: List[Pair]
+
+    @abstractmethod
+    def __init__(self, pairs: List[Pair], result_q: 'Queue[Result]'):
+        self.logger = getLogger(__name__)
+        self.pairs = pairs
+
+    @abstractmethod
+    def _write_EquityResult(self, result: EquityResult) -> None:
+        pass
+
+    @abstractmethod
+    def _write_ExecutionResult(self, result: ExecutionResult) -> None:
+        pass
+
+    @abstractmethod
+    def run(self) -> None:
+        pass
+
+
+class FileResultHandler(ResultHandler):
+    '''
+    FileResultHandler is to output results to files.
+    '''
+    pairs: List[Pair]
+    output_dir: str
+    equity_file: str
+    equity_writer: TextIO
+    execution_file: str
+    execution_writer: TextIO
+
+    def __init__(self, pairs: List[Pair], result_q: 'Queue[Result]',
+            output_dir: str):
+        self.logger = getLogger(__name__)
+        self.pairs = pairs
+        self.result_q = result_q
+        self.output_dir = output_dir
+        self.equity_file = 'Equity.csv'
+        self.equity_writer = self._create_equity_writer(self.equity_file)
+        self.execution_file = 'Execution.csv'
+        self.execution_writer = self._create_execution_writer(
+            self.execution_file
+        )
+
+    def _create_equity_writer(self, filename: str) -> TextIO:
+        _out_file = open(os.path.join(self.output_dir, filename), 'w')
+        _header: str = "Timestamp,Equity,Balance,UPL[Total]"
+        for pair in self.pairs:
+            _header += ",UPL[%s]" % pair
+        _header += "\n"
+        _out_file.write(_header)
+        self.logger.info("Created equity file. Header as: %s", _header[:-1])
+        return _out_file
+
+    def _create_execution_writer(self, filename: str) -> TextIO:
+        _out_file = open(os.path.join(self.output_dir, filename), "w")
+        _header: str = "Timestamp,Pair,Units,Price\n"
+        _out_file.write(_header)
+        self.logger.info("Created execution file. Header as: %s", _header[:-1])
+        return _out_file
+
+    def _write_EquityResult(self, result: EquityResult) -> None:
+        _line = f'{result.time},{result.equity},{result.balance}'
+        _line += f',{result.upl["total"]}'
+        for pair in self.pairs:
+            _line += f',{result.upl[pair]}'
+        _line += '\n'
+        self.equity_writer.write(_line)
+    
+    def _write_ExecutionResult(self, result: ExecutionResult) -> None:
+        _line = f'{result.time},{result.pair},{result.units},{result.price}\n'
+        self.execution_writer.write(_line)
+    
+    def _close(self) -> None:
+        self.execution_writer.close()
+        self.equity_writer.close()
+
+    def run(self) -> None:
+        self.logger.info('FileResultHandler has started running...')
+        while True:
+            try:
+                _result = self.result_q.get(block=True, timeout=5)
+            except Empty:
+                pass
+            else:
+                if _result is None:
+                    # Close worker
+                    break
+                elif isinstance(_result, EquityResult):
+                    try:
+                        self._write_EquityResult(_result)
+                    except Exception as e:
+                        self.logger.error(
+                            f'{e} - Unable to write EquityResult: {_result}'
+                        )
+                elif isinstance(_result, ExecutionResult):
+                    try:
+                        self._write_ExecutionResult(_result)
+                    except Exception as e:
+                        self.logger.error(
+                            f'{e} - Unable to write ExecutionResult: {_result}'
+                        )
+                else:
+                    self.logger.error(
+                        f'Unexpected Result has been detected: {_result}'
+                    )
+        self._close()
+        self.logger.info('FileResultHandler has completed...')
diff --git a/src/savoia/strategy/strategy.py b/src/savoia/strategy/strategy.py
index d32ffc9..62ac10e 100644
--- a/src/savoia/strategy/strategy.py
+++ b/src/savoia/strategy/strategy.py
@@ -26,7 +26,7 @@ class DummyStrategy(object):
 
     def calculate_signals(self, event: TickEvent) -> None:
         if event.type == 'TICK' and event.pair == self.pairs[0]:
-            if self.ticks % 1500 == 0:
+            if self.ticks % 100 == 0:
                 if self.invested is False:
                     signal = SignalEvent(
                         ref=str(self.ticks),
diff --git a/src/savoia/types/types.py b/src/savoia/types/types.py
index 2836e73..7d916fb 100644
--- a/src/savoia/types/types.py
+++ b/src/savoia/types/types.py
@@ -1,10 +1,11 @@
-from decimal import Decimal
-import pandas as pd
 from typing import NewType
 from typing_extensions import TypedDict
-from enum import IntFlag
+from decimal import Decimal
+import pandas as pd
+
 
 Pair = NewType('Pair', str)
+
 EventType = NewType('EventType', str)
 
 
@@ -15,13 +16,3 @@ class Price(TypedDict):
     bid: Decimal
     ask: Decimal
     time: pd.Timestamp
-
-
-class Side(IntFlag):
-    LONG = 0
-    SHORT = 1
-
-
-class TradeType(IntFlag):
-    ENTRY = 0
-    EXIT = 1


Test patch:
diff --git a/tests/engine/engine_test.py b/tests/engine/engine_test.py
index a74e11e..a588478 100644
--- a/tests/engine/engine_test.py
+++ b/tests/engine/engine_test.py
@@ -1,6 +1,6 @@
 from savoia.engine.engine import Engine, datafeed_params, \
-    execution_params, strategy_params, engine_params
-from savoia.config.dir_config import CSV_DATA_DIR
+    execution_params, strategy_params, engine_params, result_params
+from savoia.config.dir_config import CSV_DATA_DIR, OUTPUT_RESULTS_DIR
 
 from decimal import Decimal
 import logging.config
@@ -42,6 +42,13 @@ def test_engine_run() -> None:
         'params': {}
     }
 
+    result: result_params = {
+        'module_name': 'FileResultHandler',
+        'params': {
+            'output_dir': OUTPUT_RESULTS_DIR
+        }
+    }
+
     engine: engine_params = {
         'pairs': ['GBPUSD', 'USDJPY'],
         'home_currency': 'JPY',
@@ -55,7 +62,8 @@ def test_engine_run() -> None:
         engine=engine,
         datafeed=datafeed,
         execution=execution,
-        strategy=strategy
+        strategy=strategy,
+        result=result
     )
 
     setup_logging()
diff --git a/tests/event/event_test.py b/tests/event/event_test.py
index a99e036..24f31a0 100644
--- a/tests/event/event_test.py
+++ b/tests/event/event_test.py
@@ -4,7 +4,6 @@ import pandas as pd
 
 
 class TestEvent():
-    @pytest.mark.skip()
     @pytest.mark.parametrize("pair, time, bid, ask", [
         ("JPYUSD", "2018-01-16T20:31:22.579385Z", '98.02', '100.13'),
     ])
diff --git a/tests/portfolio/portfolio_test.py b/tests/portfolio/portfolio_test.py
index 554b5c0..768ebb4 100644
--- a/tests/portfolio/portfolio_test.py
+++ b/tests/portfolio/portfolio_test.py
@@ -1,5 +1,4 @@
 from decimal import Decimal
-import os
 import pytest
 
 from queue import Queue
@@ -7,9 +6,9 @@ import pandas as pd
 
 from savoia.portfolio.portfolio import Portfolio
 from savoia.ticker.ticker import Ticker
-from savoia.config.dir_config import OUTPUT_RESULTS_DIR
 from savoia.event.event import Event, SignalEvent, OrderEvent, FillEvent, \
     TickEvent
+from savoia.result.result import Result
 from savoia.types.types import Pair
 
 
@@ -37,34 +36,42 @@ def TickerMock() -> Ticker:
 def port(TickerMock: Ticker) -> Portfolio:
     ticker = TickerMock
     event_q: 'Queue[Event]' = Queue()
+    result_q: 'Queue[Result]' = Queue()
     home_currency = "JPY"
     pairs = ['GBPUSD', 'USDJPY']
     equity = Decimal("1234567")
-    port = Portfolio(ticker, event_q, home_currency, pairs, equity)
+    port = Portfolio(
+        ticker=ticker,
+        event_q=event_q,
+        result_q=result_q,
+        home_currency=home_currency,
+        pairs=pairs,
+        equity=equity
+    )
     return port
 
 
 def test_init_(port: Portfolio, TickerMock: Ticker) -> None:
     assert port.ticker is TickerMock
     assert isinstance(port.event_q, Queue)
+    assert isinstance(port.result_q, Queue)
     assert port.home_currency == 'JPY'
     assert port.equity == Decimal('1234567')
     assert port.balance == Decimal('1234567')
     assert port.upl == Decimal('0')
     assert port.pairs == ['GBPUSD', 'USDJPY']
-    assert port.isBacktest
     assert len(port.positions) == 2
 
 
-def test_create_equity_file(port: Portfolio) -> None:
-    out_file = port._create_equity_file()
-    filepath = os.path.join(OUTPUT_RESULTS_DIR, "backtest.csv")
-    assert out_file.name == str(filepath)
-    assert os.path.isfile(filepath)
+# def test_create_equity_file(port: Portfolio) -> None:
+#     out_file = port._create_equity_file()
+#     filepath = os.path.join(OUTPUT_RESULTS_DIR, "backtest.csv")
+#     assert out_file.name == str(filepath)
+#     assert os.path.isfile(filepath)
 
-    out_file.close()
-    with open(filepath, "r") as f:
-        assert f.read() == "Timestamp,Equity,GBPUSD,USDJPY\n"
+#     out_file.close()
+#     with open(filepath, "r") as f:
+#         assert f.read() == "Timestamp,Equity,GBPUSD,USDJPY\n"
 
 
 # ================================================================
@@ -119,7 +126,7 @@ def test_execute_signal_lackofticker(port: Portfolio,
             price=Decimal('103.2')
         ))
         log.check(
-            ('savoia.portfolio.portfolio', 'INFO', "Unable to execute order " +
+            ('savoia.portfolio.portfolio', 'ERROR', "Unable to execute order " +
              'as price data was insufficient.')
         )
     port.ticker.prices[pair][quote] = tmp
@@ -132,10 +139,18 @@ def test_execute_signal_lackofticker(port: Portfolio,
 def port1(TickerMock1: Ticker) -> Portfolio:
     ticker = TickerMock1
     event_q: 'Queue[Event]' = Queue()
+    result_q: 'Queue[Result]' = Queue()
     home_currency = "JPY"
     pairs = ['GBPUSD', 'USDJPY']
     equity = Decimal("100000")
-    port1 = Portfolio(ticker, event_q, home_currency, pairs, equity)
+    port1 = Portfolio(
+        ticker=ticker,
+        event_q=event_q,
+        result_q=result_q,
+        home_currency=home_currency,
+        pairs=pairs,
+        equity=equity
+    )
     return port1
 
 
@@ -177,8 +192,14 @@ def test_execute_fill_exit_entry(
         exit_price: str, exit_units: str,
         equity: str, exp_balance: str, exp_upl: str, exp_equity: str,
         TickerMock1: Ticker) -> None:
-    port = Portfolio(TickerMock1, Queue(), home_currency, ['GBPUSD', 'USDJPY'],
-        Decimal(equity), True)
+    port = Portfolio(
+        ticker=TickerMock1,
+        event_q=Queue(),
+        result_q=Queue(),
+        home_currency=home_currency,
+        pairs=['GBPUSD', 'USDJPY'],
+        equity=Decimal(equity)
+    )
     port.execute_fill(FillEvent(
         ref='ref123',
         pair=pair,
@@ -241,8 +262,8 @@ data6 = [
 def test_update_portfolio(home_currency: str, pair: Pair, exec_price: str,
         units: str, equity: str, exp_balance: str, exp_upl: str,
         exp_equity: str, TickerMock1: Ticker, TickerMock2: Ticker) -> None:
-    port = Portfolio(TickerMock1, Queue(), home_currency,
-        ["GBPUSD", "EURUSD", "USDJPY"], Decimal(equity), True)
+    port = Portfolio(TickerMock1, Queue(), Queue(), home_currency,
+        ["GBPUSD", "EURUSD", "USDJPY"], Decimal(equity))
     port.execute_fill(FillEvent(
         ref='ref123',
         pair=pair,
@@ -285,8 +306,8 @@ data7 = [
 def test_update_portfolio_home(home_currency: str, pair: Pair, exec_price: str,
         units: str, equity: str, exp_balance: str, exp_upl: str,
         exp_equity: str, TickerMock1: Ticker, TickerMock2: Ticker) -> None:
-    port = Portfolio(TickerMock1, Queue(), home_currency,
-        ["GBPUSD", "EURUSD", "USDJPY"], Decimal(equity), True)
+    port = Portfolio(TickerMock1, Queue(), Queue(), home_currency,
+        ["GBPUSD", "EURUSD", "USDJPY"], Decimal(equity))
     port.execute_fill(FillEvent(
         ref='ref123',
         pair=pair,
diff --git a/tests/result/result_test.py b/tests/result/result_test.py
new file mode 100644
index 0000000..c77ca02
--- /dev/null
+++ b/tests/result/result_test.py
@@ -0,0 +1,68 @@
+import pytest
+import py
+import os
+
+from savoia.result.result import EquityResult, ExecutionResult, \
+    FileResultHandler, Result
+
+from queue import Queue
+import pandas as pd
+from decimal import Decimal
+
+
+# =============================================================
+# FileResultHandler
+# =============================================================
+def test_init_(tmpdir: py.path.local) -> None:
+    pairs = ['GBPUSD', 'USDJPY']
+    result_q: 'Queue[Result]' = Queue()
+    equity_file = tmpdir.join('Equity.csv')
+    execution_file = tmpdir.join('Execution.csv')
+
+    frh = FileResultHandler(pairs, result_q, tmpdir)
+    frh._close()
+
+    assert os.path.isfile(equity_file)
+    assert os.path.isfile(execution_file)
+
+    assert equity_file.read() == 'Timestamp,Equity,Balance,UPL[Total],' + \
+        'UPL[GBPUSD],UPL[USDJPY]\n'
+    assert execution_file.read() == 'Timestamp,Pair,Units,Price\n'
+
+
+def test_run(tmpdir: py.path.local) -> None:
+    pairs = ['GBPUSD', 'USDJPY']
+    result_q: 'Queue[Result]' = Queue()
+    equity_file = tmpdir.join('Equity.csv')
+    execution_file = tmpdir.join('Execution.csv')
+    equity_result = EquityResult(
+        pd.Timestamp('2020-07-15 22:18:23'),
+        Decimal('111.1'),
+        Decimal('2222.22'),
+        {
+            'total': Decimal('33.333'),
+            'GBPUSD': Decimal('4.4444'),
+            'USDJPY': Decimal('5.55555')
+        }
+    )
+    execution_result = ExecutionResult(
+        pd.Timestamp('2020-07-14 22:20:00'),
+        'USDJPY',
+        Decimal('2.22'),
+        Decimal('99.9')
+    )
+
+    frh = FileResultHandler(pairs, result_q, tmpdir)
+    result_q.put(equity_result)
+    result_q.put(execution_result)
+    result_q.put(None)
+    frh.run()
+
+    equity_file_result = equity_file.readlines()
+    execution_file_result = execution_file.readlines()
+    assert equity_file_result[0] == 'Timestamp,Equity,Balance,UPL[Total],' + \
+        'UPL[GBPUSD],UPL[USDJPY]\n'
+    assert equity_file_result[1] == \
+        '2020-07-15 22:18:23,111.1,2222.22,33.333,4.4444,5.55555\n'
+    assert execution_file_result[0] == 'Timestamp,Pair,Units,Price\n'
+    assert execution_file_result[1] == '2020-07-14 22:20:00,USDJPY,2.22,99.9\n'