diff --git a/src/naiback/broker/broker.py b/src/naiback/broker/broker.py index 4a60cde..2a03759 100644 --- a/src/naiback/broker/broker.py +++ b/src/naiback/broker/broker.py @@ -1,4 +1,6 @@ +from naiback.broker.position import Position + class Broker: """ Broker has several responsibilities (so called SRP, or several responsibilities principle): @@ -9,6 +11,27 @@ class Broker: """ def __init__(self, initial_cash=100000.): - self.cash = initial_cash + self.cash_ = initial_cash self.positions = [] + self.commission_percentage = 0 + + def cash(self): + return self.cash_ + + def add_position(self, ticker, price, amount): + volume = abs(price * amount) + if amount > 0: + if volume * (1 + self.commission_percentage) > self.cash_: + return None + pos = Position(ticker) + pos.enter(price, amount) + self.cash_ -= (volume + volume * self.commission_percentage) + self.positions.append(pos) + return pos + + def set_commission(self, percentage): + self.commission_percentage = percentage + + def all_positions(self): + return self.positions diff --git a/src/naiback/broker/position.py b/src/naiback/broker/position.py index 0a42c11..3742fe6 100644 --- a/src/naiback/broker/position.py +++ b/src/naiback/broker/position.py @@ -3,13 +3,22 @@ class Position: def __init__(self, ticker): self.ticker = ticker - self.entry_price = None + self.entry_price_ = None self.entry_metadata = {} - self.exit_price = None + self.exit_price_ = None self.exit_metadata = {} - self.size = None + self.size_ = None self.total_pnl = 0 + def entry_price(self): + return self.entry_price_ + + def exit_price(self): + return self.exit_price_ + + def size(self): + return self.size_ + def entry_commission(self): return self.entry_metadata['commission'] @@ -20,15 +29,15 @@ class Position: return self.total_pnl def enter(self, price, amount, **kwargs): - self.entry_price = price - self.size = amount + self.entry_price_ = price + self.size_ = amount for k, v in kwargs.items(): self.entry_metadata[k] = v def exit(self, price): - self.exit_price = price - self.total_pnl += (self.exit_price - self.entry_price) * self.size - self.size = 0 + self.exit_price_ = price + self.total_pnl += (self.exit_price() - self.entry_price()) * self.size() + self.size_ = 0 diff --git a/tests/test_broker.py b/tests/test_broker.py new file mode 100644 index 0000000..df08f9b --- /dev/null +++ b/tests/test_broker.py @@ -0,0 +1,43 @@ + +import pytest +import datetime + +from naiback.broker.broker import Broker + +def test_broker_cash(): + broker = Broker(initial_cash=100000.) + + assert broker.cash() == 100000. + +def test_broker_add_position_enough_cash(): + broker = Broker(initial_cash=100) + + pos = broker.add_position('FOO', price=10, amount=1) + + assert pos.entry_price() == 10 + assert pos.size() == 1 + +def test_broker_add_position_not_enough_cash(): + broker = Broker(initial_cash=100) + + pos = broker.add_position('FOO', price=1000, amount=1) + + assert pos is None + +def test_broker_percentage_commissions(): + broker = Broker(initial_cash=100) + broker.set_commission(percentage=0.05) # 0.05% + + pos = broker.add_position('FOO', price=10, amount=1) + + should_be_cash = 100 - 10 - 10 * 0.01 * 0.05 + + assert (broker.cash() - should_be_cash) < 0.00001 + +def test_broker_all_position(): + broker = Broker(initial_cash=100) + + pos = broker.add_position('FOO', price=10, amount=1) + + assert pos in broker.all_positions() + diff --git a/tests/test_position.py b/tests/test_position.py index 5ce6d20..b048b30 100644 --- a/tests/test_position.py +++ b/tests/test_position.py @@ -11,8 +11,8 @@ def position(): def test_position_enter(position): position.enter(3.50, 10) - assert position.entry_price == 3.50 - assert position.size == 10 + assert position.entry_price() == 3.50 + assert position.size() == 10 def test_position_enter_metadata(position): position.enter(3.50, 10, commission=0.1, bar=42) @@ -30,14 +30,14 @@ def test_position_exit(position): position.enter(3.50, 10) position.exit(4.50) - assert position.exit_price == 4.50 - assert position.size == 0 + assert position.exit_price() == 4.50 + assert position.size() == 0 def test_position_enter_short(position): position.enter(3.50, -10) - assert position.entry_price == 3.50 - assert position.size == -10 + assert position.entry_price() == 3.50 + assert position.size() == -10 def test_position_exit_pnl(position): position.enter(3.50, 10)