Repository: Ma-r-co/savoia

Issue:
Unbundle Ticker from datafeed
Unbundle Ticker from datafeed
 - datafeed is supposed to be in a separate thread for multi threadhing.
 - Ticker should be in a same thread as main (portfolio, strategy, and so on..)

Discussion:

Patch:
diff --git a/src/savoia/datafeed/datafeed.py b/src/savoia/datafeed/datafeed.py
index cdb9ffe..809b1a5 100644
--- a/src/savoia/datafeed/datafeed.py
+++ b/src/savoia/datafeed/datafeed.py
@@ -1,7 +1,7 @@
 from decimal import Decimal
 from savoia.config.decimal_config \
     import initializeDecimalContext, DECIMAL_PLACES
-from savoia.datafeed.ticker import Ticker
+from savoia.ticker.ticker import Ticker
 
 import os
 import re
diff --git a/src/savoia/portfolio/_position copy.py b/src/savoia/portfolio/_position copy.py
deleted file mode 100644
index 64b297d..0000000
--- a/src/savoia/portfolio/_position copy.py
+++ /dev/null
@@ -1,127 +0,0 @@
-from decimal import Decimal
-from savoia.config.decimal_config \
-    import initializeDecimalContext, DECIMAL_PLACES
-from savoia.datafeed.ticker import Ticker
-from savoia.types.types import Pair
-
-
-class Position():
-    home_currency: str
-    position_type: str
-    currency_pair: Pair
-    units: int
-    ticker: Ticker
-    profit_base: Decimal
-    profit_perc: Decimal
-    base_currency: str
-    quote_currency: str
-    quote_home_currency_pair: Pair
-
-    def __init__(
-        self, home_currency: str, position_type: str,
-        currency_pair: Pair, units: int, ticker: Ticker
-    ) -> None:
-        self.home_currency = home_currency
-        self.position_type = position_type
-        self.currency_pair = currency_pair
-        self.units = units
-        self.ticker = ticker
-        self._set_up_currencies()
-        self.profit_base = self._calculate_profit_base()
-        self.profit_perc = self._calculate_profit_perc()
-        initializeDecimalContext()
-
-    def _set_up_currencies(self) -> None:
-        self.base_currency = self.currency_pair[:3]
-        self.quote_currency = self.currency_pair[3:]
-        if self.quote_currency == self.home_currency:
-            self.quote_home_currency_pair = self.currency_pair
-        else:
-            self.quote_home_currency_pair = "%s%s" % \
-                (self.quote_currency, self.home_currency)
-
-        ticker_cur = self.ticker.prices[self.currency_pair]
-        if self.position_type == "long":
-            self.avg_price = Decimal(str(ticker_cur["ask"]))
-            self.cur_price = Decimal(str(ticker_cur["bid"]))
-        else:
-            self.avg_price = Decimal(str(ticker_cur["bid"]))
-            self.cur_price = Decimal(str(ticker_cur["ask"]))
-
-    def _calculate_pips(self) -> Decimal:
-        if self.position_type == "long":
-            mult = Decimal("1")
-        elif self.position_type == "short":
-            mult = Decimal("-1")
-        else:
-            raise ValueError("Expected 'long' or 'short'. position_type = %s"
-                % str(self.position_type))
-        pips = (mult * (self.cur_price - self.avg_price)).quantize(
-            DECIMAL_PLACES)
-        return pips
-
-    def _calculate_profit_base(self) -> Decimal:
-        pips = self._calculate_pips()
-        if self.quote_home_currency_pair == self.currency_pair:
-            profit = pips * self.units
-        else:
-            ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
-            if self.position_type == "long":
-                qh_close = ticker_qh["bid"]
-            else:
-                qh_close = ticker_qh["ask"]
-            profit = pips * qh_close * self.units
-        return profit.quantize(DECIMAL_PLACES)
-
-    def _calculate_profit_perc(self) -> Decimal:
-        pips = self._calculate_pips()
-        return (pips / self.avg_price * Decimal("100.00")).quantize(
-            DECIMAL_PLACES)
-
-    def update_position_price(self) -> None:
-        ticker_cur = self.ticker.prices[self.currency_pair]
-        if self.position_type == "long":
-            self.cur_price = Decimal(str(ticker_cur["bid"]))
-        else:
-            self.cur_price = Decimal(str(ticker_cur["ask"]))
-        self.profit_base = self._calculate_profit_base()
-        self.profit_perc = self._calculate_profit_perc()
-
-    def add_units(self, units: int) -> None:
-        cp = self.ticker.prices[self.currency_pair]
-        if self.position_type == "long":
-            add_price = cp["ask"]
-        else:
-            add_price = cp["bid"]
-        new_total_units = self.units + units
-        new_total_cost = self.avg_price * self.units + add_price * units
-        self.avg_price = (new_total_cost / new_total_units).quantize(
-            DECIMAL_PLACES)
-        self.units = new_total_units
-        self.update_position_price()
-
-    def remove_units(self, units: int) -> Decimal:
-        self.units -= units
-        self.update_position_price()
-        if self.quote_home_currency_pair == self.currency_pair:
-            pnl = self._calculate_pips() * units
-        else:
-            ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
-            if self.position_type == "long":
-                qh_close = ticker_qh["bid"]
-            else:
-                qh_close = ticker_qh["ask"]
-            pnl = self._calculate_pips() * qh_close * units
-        return pnl.quantize(Decimal("0.01"))
-
-    def close_position(self) -> Decimal:
-        if self.quote_home_currency_pair == self.currency_pair:
-            pnl = self._calculate_pips() * self.units
-        else:
-            ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
-            if self.position_type == "long":
-                qh_close = ticker_qh["bid"]
-            else:
-                qh_close = ticker_qh["ask"]
-            pnl = self._calculate_pips() * qh_close * self.units
-        return pnl.quantize(Decimal("0.01"))
diff --git a/src/savoia/portfolio/portfolio.py b/src/savoia/portfolio/portfolio.py
index 2709b2a..4745f5a 100644
--- a/src/savoia/portfolio/portfolio.py
+++ b/src/savoia/portfolio/portfolio.py
@@ -4,7 +4,7 @@ import os
 
 import pandas as pd
 
-from savoia.datafeed.ticker import Ticker
+from savoia.ticker.ticker import Ticker
 from savoia.event.event import OrderEvent, Event, TickEvent, \
     SignalEvent, FillEvent
 from savoia.performance.performance import create_drawdowns
diff --git a/src/savoia/portfolio/position.py b/src/savoia/portfolio/position.py
index 204ad2c..5e3930d 100644
--- a/src/savoia/portfolio/position.py
+++ b/src/savoia/portfolio/position.py
@@ -1,6 +1,6 @@
 from decimal import Decimal
 from savoia.config.decimal_config import DECIMAL_PLACES
-from savoia.datafeed.ticker import Ticker
+from savoia.ticker.ticker import Ticker
 from savoia.portfolio.trade import Trade
 from savoia.types.types import Pair, Price
 
diff --git a/src/savoia/datafeed/ticker.py b/src/savoia/ticker/ticker.py
similarity index 70%
rename from src/savoia/datafeed/ticker.py
rename to src/savoia/ticker/ticker.py
index e49bff0..a869406 100644
--- a/src/savoia/datafeed/ticker.py
+++ b/src/savoia/ticker/ticker.py
@@ -1,10 +1,10 @@
 from decimal import Decimal
 from savoia.config.decimal_config import DECIMAL_PLACES
 
-# import numpy as np
 import pandas as pd
 
 from savoia.types.types import Pair, Price
+from savoia.event.event import TickEvent
 
 from logging import getLogger, Logger
 from typing import List, Dict, Tuple
@@ -50,3 +50,22 @@ class Ticker(object):
         inv_bid = (Decimal("1.0") / ask).quantize(DECIMAL_PLACES)
         inv_ask = (Decimal("1.0") / bid).quantize(DECIMAL_PLACES)
         return inv_pair, inv_bid, inv_ask
+
+    def update_ticker(self, event: TickEvent) -> None:
+        '''Updates prices upon TickEvent'''
+        _pair: Pair
+        _bid: Decimal
+        _ask: Decimal
+
+        if event.type != 'TICK':
+            raise Exception(f"Incorrect EventType: {event.type}, " +
+                "expected 'TICK' event.")
+        else:
+            _pair = event.instrument
+            _bid = event.bid
+            _ask = event.ask
+            self.prices[_pair]['bid'] = _bid
+            self.prices[_pair]['ask'] = _ask
+            inv_pair, inv_bid, inv_ask = self.invert_prices(_pair, _bid, _ask)
+            self.prices[inv_pair]["bid"] = inv_bid
+            self.prices[inv_pair]["ask"] = inv_ask


Test patch:
diff --git a/src/savoia/backtest/backtest.py b/src/savoia/backtest/backtest.py
index b76ff66..02b83ee 100644
--- a/src/savoia/backtest/backtest.py
+++ b/src/savoia/backtest/backtest.py
@@ -5,7 +5,7 @@ from savoia.config.dir_config import CSV_DATA_DIR
 from savoia.types.types import Pair
 from savoia.event.event import Event
 from savoia.datafeed.datafeed import DataFeeder
-from savoia.datafeed.ticker import Ticker
+from savoia.ticker.ticker import Ticker
 from savoia.strategy.strategy import Strategy
 from savoia.portfolio.portfolio import Portfolio
 from savoia.execution.execution import SimulatedExecution
diff --git a/tests/backtest/backtest_test.py b/tests/backtest/backtest_test.py
index f65f870..1e6d22e 100644
--- a/tests/backtest/backtest_test.py
+++ b/tests/backtest/backtest_test.py
@@ -3,7 +3,7 @@ from savoia.backtest.backtest import Backtest
 from savoia.types.types import Pair
 from savoia.event.event import Event, TickEvent
 from savoia.datafeed.datafeed import DataFeeder
-from savoia.datafeed.ticker import Ticker
+from savoia.ticker.ticker import Ticker
 from savoia.strategy.strategy import DummyStrategy, Strategy
 from savoia.portfolio.portfolio import Portfolio
 from savoia.execution.execution import SimulatedExecution
diff --git a/tests/datafeed/datafeed_test.py b/tests/datafeed/datafeed_test.py
index 4e481fe..c89bd77 100644
--- a/tests/datafeed/datafeed_test.py
+++ b/tests/datafeed/datafeed_test.py
@@ -1,6 +1,6 @@
 import pytest
 from savoia.datafeed.datafeed import DataFeeder, HistoricCSVDataFeeder
-from savoia.datafeed.ticker import Ticker
+from savoia.ticker.ticker import Ticker
 from queue import Queue
 from decimal import Decimal
 import pandas as pd
@@ -16,42 +16,9 @@ def setupDataFeeder() -> Tuple[Ticker, DataFeeder]:
     return ticker, datafeeder
 
 
-# ---------------------------------------------------------------
-# Ticker
-# ---------------------------------------------------------------
-def test_set_up_prices_dict(setupDataFeeder: Tuple[Ticker, DataFeeder]) -> None:
-    """
-    _set_up_prices_dict should return both prices dict and
-    inv-prices dict
-    """
-    ticker, df = setupDataFeeder
-    prices_dict = ticker._set_up_prices_dict()
-    assert sorted(list(prices_dict.keys())) == sorted(
-        ["USDJPY", "JPYUSD", "GBPUSD", "USDGBP"])
-    for v in prices_dict.values():
-        assert v == {'bid': Decimal(0), 'ask': Decimal(0),
-                    'time': pd.Timestamp(0)}
-
-
-@pytest.mark.parametrize('pair, bid, ask, return_pair, return_bid, return_ask',
-                    [("USDJPY", Decimal('106.87'), Decimal('106.90'),
-                    "JPYUSD", Decimal('0.00935454'), Decimal('0.00935716')),
-                    ("EURGBP", Decimal('0.90473'), Decimal('0.90561'),
-                    "GBPEUR", Decimal('1.10422809'), Decimal('1.10530213'))])
-def test_invert_prices(setupDataFeeder: Tuple[Ticker, DataFeeder], pair: str,
-        bid: Decimal, ask: Decimal, return_pair: str, return_bid: Decimal,
-        return_ask: Decimal) -> None:
-    """ invert_prices should return inverted pair, bid and ask prices"""
-    ticker, df = setupDataFeeder
-    inv_pair, inv_bid, inv_ask = ticker.invert_prices(pair, bid, ask)
-    assert inv_pair == return_pair
-    assert inv_bid == return_bid
-    assert inv_ask == return_ask
-
 # ---------------------------------------------------------------
 # DataFeeder
 # ---------------------------------------------------------------
-
 def test_list_all_csv_files(setupDataFeeder: Tuple[Ticker, DataFeeder]) -> None:
     """_list_all_csv_files should return a sorted list of csv files in a
     directory
@@ -66,7 +33,8 @@ def test_list_all_csv_files(setupDataFeeder: Tuple[Ticker, DataFeeder]) -> None:
     ])
 
 
-def test_list_all_file_dates(setupDataFeeder: Tuple[Ticker, DataFeeder]) -> None:
+def test_list_all_file_dates(setupDataFeeder: Tuple[Ticker, DataFeeder]) \
+        -> None:
     """_list_all_file_dates should return a sorted and not-duplicated list of
     dates in the feed directory.
     """
diff --git a/tests/portfolio/portfolio_test.py b/tests/portfolio/portfolio_test.py
index aaad833..2b95d99 100644
--- a/tests/portfolio/portfolio_test.py
+++ b/tests/portfolio/portfolio_test.py
@@ -6,7 +6,7 @@ from queue import Queue
 import pandas as pd
 
 from savoia.portfolio.portfolio import Portfolio
-from savoia.datafeed.ticker import Ticker
+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
diff --git a/tests/portfolio/position_test.py b/tests/portfolio/position_test.py
index 253699c..8709ff4 100644
--- a/tests/portfolio/position_test.py
+++ b/tests/portfolio/position_test.py
@@ -1,6 +1,6 @@
 from savoia.portfolio.position import Position
 # from savoia.portfolio.trade import Trade
-from savoia.datafeed.ticker import Ticker
+from savoia.ticker.ticker import Ticker
 from savoia.types.types import Pair
 
 from decimal import Decimal
diff --git a/tests/ticker/ticker_test.py b/tests/ticker/ticker_test.py
new file mode 100644
index 0000000..4dd8555
--- /dev/null
+++ b/tests/ticker/ticker_test.py
@@ -0,0 +1,70 @@
+import pytest
+from savoia.ticker.ticker import Ticker
+from savoia.event.event import TickEvent
+from decimal import Decimal
+import pandas as pd
+
+
+# ---------------------------------------------------------------
+# Ticker
+# ---------------------------------------------------------------
+def test_set_up_prices_dict() -> None:
+    """
+    _set_up_prices_dict should return both prices dict and
+    inv-prices dict
+    """
+    pairs = ['USDJPY', 'GBPUSD']
+    ticker = Ticker(pairs)
+    prices_dict = ticker._set_up_prices_dict()
+    assert sorted(list(prices_dict.keys())) == sorted(
+        ["USDJPY", "JPYUSD", "GBPUSD", "USDGBP"])
+    for v in prices_dict.values():
+        assert v == {'bid': Decimal(0), 'ask': Decimal(0),
+                    'time': pd.Timestamp(0)}
+
+
+@pytest.mark.parametrize('pair, bid, ask, return_pair, return_bid, return_ask',
+                    [("USDJPY", Decimal('106.87'), Decimal('106.90'),
+                    "JPYUSD", Decimal('0.00935454'), Decimal('0.00935716')),
+                    ("EURGBP", Decimal('0.90473'), Decimal('0.90561'),
+                    "GBPEUR", Decimal('1.10422809'), Decimal('1.10530213'))])
+def test_invert_prices(pair: str,
+        bid: Decimal, ask: Decimal, return_pair: str, return_bid: Decimal,
+        return_ask: Decimal) -> None:
+    """ invert_prices should return inverted pair, bid and ask prices"""
+    pairs = ['USDJPY', 'GBPUSD']
+    ticker = Ticker(pairs)
+    inv_pair, inv_bid, inv_ask = ticker.invert_prices(pair, bid, ask)
+    assert inv_pair == return_pair
+    assert inv_bid == return_bid
+    assert inv_ask == return_ask
+
+
+def test_update_ticker() -> None:
+    pairs = ['GBPUSD', 'USDJPY']
+    time = pd.Timestamp('2020-07-09 12:23:10')
+    ticker = Ticker(pairs)
+
+    # pair, ask, bid, GBPUSD(ask, bid), USDGBP(ask, bid),
+    # USDJPY(ask, bid), JPYUSD(ask, bid)
+    tick1 = ['GBPUSD', '1.50349', '1.30328', '1.50349', '1.30328',
+            '0.76729483', '0.66511916', '0', '0', '0', '0']
+    tick2 = ['USDJPY', '110.863', '105.774', '1.50349', '1.30328',
+            '0.76729483', '0.66511916', '110.863', '105.774',
+            '0.00945412', '0.00902014']
+    tick3 = ['GBPUSD', '1.2543', '1.2541', '1.2543', '1.2541',
+            '0.79738458', '0.79725743', '110.863', '105.774',
+            '0.00945412', '0.00902014']
+    tick4 = ['USDJPY', '107.8', '107.25', '1.2543', '1.2541',
+            '0.79738458', '0.79725743', '107.8', '107.25',
+            '0.00932401', '0.00927644']
+    tick = [tick1, tick2, tick3, tick4]
+
+    for t in tick:
+        event = TickEvent(t[0], time, Decimal(t[2]), Decimal(t[1]))
+        ticker.update_ticker(event)
+        result = []
+        for p in ['GBPUSD', 'USDGBP', 'USDJPY', 'JPYUSD']:
+            for q in ['ask', 'bid']:
+                result.append(ticker.prices[p][q])
+        assert result == list(map(Decimal, t[3:]))