-
This commit is contained in:
58
strategy/buy_and_hold.py
Normal file
58
strategy/buy_and_hold.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import math
|
||||
from enum import Enum, auto
|
||||
from typing import override
|
||||
|
||||
from internal_types.types import OHLC, BidAsk, Instrument, Position
|
||||
from strategy.strategy import Strategy
|
||||
from utils.utils import Portfolio
|
||||
|
||||
|
||||
class State(Enum):
|
||||
POS_0 = auto()
|
||||
POS_1 = auto()
|
||||
|
||||
|
||||
class BuyAndHold(Strategy):
|
||||
|
||||
def __init__(self, init_balance, instr: Instrument):
|
||||
self.state = State.POS_0
|
||||
self.balance = init_balance
|
||||
self.instr = instr
|
||||
self.portfolio = Portfolio()
|
||||
self.desired_portfolio = Portfolio()
|
||||
|
||||
def __process(self, at_price: float):
|
||||
match self.state:
|
||||
case State.POS_0:
|
||||
quantity = math.floor(self.balance / at_price / self.instr.multiplier)
|
||||
self.desired_portfolio.add_position(Position(self.instr, quantity, at_price))
|
||||
self.state = State.POS_1
|
||||
case State.POS_1:
|
||||
pass
|
||||
case _:
|
||||
raise RuntimeError('invalid state')
|
||||
|
||||
@override
|
||||
def unfilled_positions(self, instr: Instrument) -> Position:
|
||||
x = self.desired_portfolio.outstanding_shares(instr) - self.portfolio.outstanding_shares(instr)
|
||||
return self.desired_portfolio.consolidate_last_x_shares(instr, x)
|
||||
|
||||
@override
|
||||
def order_filled(self, new_pos: Position):
|
||||
self.portfolio.add_position(new_pos)
|
||||
|
||||
@override
|
||||
def process_bid_ask(self, bid_ask: BidAsk):
|
||||
if bid_ask.instr != self.instr:
|
||||
return
|
||||
self.__process(bid_ask.ask)
|
||||
|
||||
@override
|
||||
def process_ohlc(self, ohlc: OHLC):
|
||||
if ohlc.instr != self.instr:
|
||||
return
|
||||
self.__process(ohlc.close)
|
||||
|
||||
@override
|
||||
def net_liquid_value(self, at_price: float) -> float:
|
||||
return self.balance + self.portfolio.total_gains(self.instr, at_price)
|
||||
92
strategy/sma_crossover.py
Normal file
92
strategy/sma_crossover.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import math
|
||||
from enum import Enum, auto
|
||||
from typing import List, override
|
||||
|
||||
from internal_types.types import OHLC, BidAsk, Instrument, Position
|
||||
from strategy.strategy import Strategy
|
||||
from utils.utils import SEC_1_HOUR, SMA, Portfolio
|
||||
|
||||
|
||||
class State(Enum):
|
||||
POS_0 = auto()
|
||||
POS_1 = auto()
|
||||
|
||||
|
||||
class Cross(Enum):
|
||||
UNSPECIFIED = auto()
|
||||
GOLDEN = auto()
|
||||
DEATH = auto()
|
||||
|
||||
|
||||
class SMACrossover(Strategy):
|
||||
|
||||
def __init__(self,
|
||||
init_balance,
|
||||
instr: Instrument,
|
||||
interval_sec: int,
|
||||
short_window_sec: int = 12 * SEC_1_HOUR,
|
||||
long_window_sec: int = 26 * SEC_1_HOUR):
|
||||
self.state = State.POS_0
|
||||
self.init_balance = init_balance
|
||||
self.instr = instr
|
||||
self.short_sma = SMA(interval_sec, short_window_sec)
|
||||
self.long_sma = SMA(interval_sec, long_window_sec)
|
||||
self.portfolio = Portfolio()
|
||||
self.desired_portfolio = Portfolio()
|
||||
|
||||
def __cross(self) -> Cross:
|
||||
return Cross.GOLDEN if self.short_sma.val() > self.long_sma.val() else Cross.DEATH
|
||||
|
||||
def __unit_limit(self, price_at: float) -> int:
|
||||
return math.floor(self.net_liquid_value(price_at) / price_at / self.instr.multiplier)
|
||||
|
||||
def warmup(self, warmup_historical_data: List[OHLC]):
|
||||
for ohlc in warmup_historical_data:
|
||||
self.short_sma.append(ohlc.close)
|
||||
self.long_sma.append(ohlc.close)
|
||||
|
||||
if not (self.short_sma.has_val() and self.long_sma.has_val()):
|
||||
raise ValueError('need as least `long_window_sec` of OHLC to warmup')
|
||||
|
||||
self.state = State.POS_1
|
||||
|
||||
@override
|
||||
def unfilled_positions(self, instr: Instrument) -> Position:
|
||||
x = self.desired_portfolio.outstanding_shares(instr) - self.portfolio.outstanding_shares(instr)
|
||||
return self.desired_portfolio.consolidate_last_x_shares(instr, x)
|
||||
|
||||
@override
|
||||
def order_filled(self, new_pos: Position):
|
||||
self.portfolio.add_position(new_pos)
|
||||
|
||||
@override
|
||||
def process_bid_ask(self, bid_ask: BidAsk):
|
||||
assert False, 'todo'
|
||||
|
||||
@override
|
||||
def process_ohlc(self, ohlc: OHLC):
|
||||
if self.state == State.POS_0:
|
||||
raise RuntimeError('strategy wasn\'t warmed up yet')
|
||||
|
||||
if ohlc.instr != self.instr:
|
||||
return
|
||||
|
||||
prev_cross = self.__cross()
|
||||
|
||||
self.short_sma.append(ohlc.close)
|
||||
self.long_sma.append(ohlc.close)
|
||||
|
||||
curr_cross = self.__cross()
|
||||
|
||||
if prev_cross != curr_cross:
|
||||
outstanding_shares = self.desired_portfolio.outstanding_shares(self.instr)
|
||||
desired_shares = self.__unit_limit(ohlc.close)
|
||||
if curr_cross == Cross.DEATH:
|
||||
# desired_shares = 0 # long only
|
||||
desired_shares = -desired_shares
|
||||
quantity = desired_shares - outstanding_shares
|
||||
self.desired_portfolio.add_position(Position(self.instr, quantity, ohlc.close))
|
||||
|
||||
@override
|
||||
def net_liquid_value(self, at_price: float) -> float:
|
||||
return self.init_balance + self.portfolio.total_gains(self.instr, at_price)
|
||||
33
strategy/strategy.py
Normal file
33
strategy/strategy.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from internal_types.types import OHLC, BidAsk, Instrument, Position
|
||||
|
||||
|
||||
# todo:
|
||||
# handle strategy that trades multiple instruments
|
||||
# fractional position/volume for crypto
|
||||
# callback for async order
|
||||
#
|
||||
# def instruments_to_trade
|
||||
# def 'stuff' to stream (stock price, vix, ...)
|
||||
class Strategy(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def unfilled_positions(self, instr: Instrument) -> Position:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def order_filled(self, new_pos: Position):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def process_bid_ask(self, bid_ask: BidAsk):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def process_ohlc(self, ohlc: OHLC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def net_liquid_value(self, at_price: float) -> float:
|
||||
pass
|
||||
234
strategy/turtle_system_1.py
Normal file
234
strategy/turtle_system_1.py
Normal file
@@ -0,0 +1,234 @@
|
||||
import math
|
||||
from enum import Enum, auto
|
||||
from typing import List, override
|
||||
|
||||
import numpy as np
|
||||
|
||||
from internal_types.types import OHLC, BidAsk, Instrument, Position, Quote
|
||||
from strategy.strategy import Strategy
|
||||
from utils.utils import SEC_1_DAY, BlendedOHLC, Portfolio, crossover, crossunder, max_abs
|
||||
|
||||
|
||||
class State(Enum):
|
||||
WARMUP = auto()
|
||||
INIT = auto()
|
||||
LONG = auto()
|
||||
SHORT = auto()
|
||||
SHADOW_LONG = auto()
|
||||
SHADOW_SHORT = auto()
|
||||
|
||||
|
||||
class TurtleSystem1(Strategy):
|
||||
|
||||
def __init__(self, init_balance: float, instr: Instrument):
|
||||
self.state = State.WARMUP
|
||||
self.init_balance = init_balance
|
||||
self.N: float = 0
|
||||
self.tradable_acct_balance = init_balance
|
||||
self.unit_size: int = 0
|
||||
self.instr = instr
|
||||
self.unit_limit = 4
|
||||
self.trade_next_20_day_breakout = True
|
||||
self.daily_ohlc = BlendedOHLC(instr, SEC_1_DAY)
|
||||
self.shadow_portfolio = Portfolio()
|
||||
self.portfolio = Portfolio()
|
||||
self.desired_portfolio = Portfolio()
|
||||
|
||||
def warmup(self, warmup_historical_data: List[OHLC]):
|
||||
for ohlc in warmup_historical_data:
|
||||
self.daily_ohlc.append(ohlc)
|
||||
|
||||
if len(self.daily_ohlc) < 20:
|
||||
raise ValueError('need as least 20 days of OHLC to warmup')
|
||||
|
||||
self.__init_day_20_N()
|
||||
self.__adjust_unit_size()
|
||||
self.state = State.INIT
|
||||
|
||||
def __true_range(self, day_i) -> float:
|
||||
# todo: check index out of bound
|
||||
curr_q = self.daily_ohlc.blended_ohlc(day_i)
|
||||
if (day_i + len(self.daily_ohlc)) % len(self.daily_ohlc) == 0:
|
||||
return curr_q.high - curr_q.low
|
||||
prev_q = self.daily_ohlc.blended_ohlc(day_i - 1)
|
||||
return max_abs(curr_q.high - curr_q.low, curr_q.high - prev_q.close, prev_q.close - curr_q.low)
|
||||
|
||||
def __init_day_20_N(self):
|
||||
days = min(len(self.daily_ohlc), 20)
|
||||
self.N = float(np.mean([self.__true_range(day_i) for day_i in range(-days, 0)]))
|
||||
|
||||
def __adjust_unit_size(self):
|
||||
if self.N == 0:
|
||||
raise RuntimeError('N can\'t be 0')
|
||||
elif self.instr.multiplier == 0:
|
||||
raise RuntimeError('multiplier can\'t be 0')
|
||||
|
||||
self.unit_size = \
|
||||
math.floor(self.tradable_acct_balance * 0.01 / (self.N * self.instr.multiplier))
|
||||
|
||||
def __reach_unit_limit(self):
|
||||
outstanding_shares = self.desired_portfolio.outstanding_shares(self.instr)
|
||||
return abs(outstanding_shares) >= self.unit_limit * self.unit_size
|
||||
|
||||
def __shadow_reach_unit_limit(self):
|
||||
shadow_outstanding_shares = self.shadow_portfolio.outstanding_shares(self.instr)
|
||||
return abs(shadow_outstanding_shares) >= self.unit_limit * self.unit_size
|
||||
|
||||
def __submit_order(self, quantity: int, price: float):
|
||||
self.desired_portfolio.add_position(Position(self.instr, quantity, price))
|
||||
|
||||
def __shadow_submit_order(self, quantity: int, price: float):
|
||||
self.shadow_portfolio.add_position(Position(self.instr, quantity, price))
|
||||
|
||||
def __last_order_price(self) -> float:
|
||||
return self.desired_portfolio.pos_history[self.instr][-1].price
|
||||
|
||||
def __shadow_last_order_price(self) -> float:
|
||||
return self.shadow_portfolio.pos_history[self.instr][-1].price
|
||||
|
||||
def __to_INIT_state(self, price: float):
|
||||
self.desired_portfolio.liquidate_positions(self.instr, price)
|
||||
self.shadow_portfolio = Portfolio()
|
||||
self.state = State.INIT
|
||||
|
||||
def __to_LONG_state(self, price: float):
|
||||
self.__submit_order(self.unit_size, price)
|
||||
self.shadow_portfolio = Portfolio()
|
||||
self.state = State.LONG
|
||||
|
||||
def __to_SHORT_state(self, price: float):
|
||||
self.__submit_order(-self.unit_size, price)
|
||||
self.shadow_portfolio = Portfolio()
|
||||
self.state = State.SHORT
|
||||
|
||||
def __to_SHADOW_LONG_state(self, price: float):
|
||||
self.shadow_portfolio = Portfolio()
|
||||
self.__shadow_submit_order(self.unit_size, price)
|
||||
self.state = State.SHADOW_LONG
|
||||
|
||||
def __to_SHADOW_SHORT_state(self, price: float):
|
||||
self.shadow_portfolio = Portfolio()
|
||||
self.__shadow_submit_order(self.unit_size, price)
|
||||
self.state = State.SHADOW_SHORT
|
||||
|
||||
def __process_INIT_state(self, quote: Quote):
|
||||
breakthrough_20_high = self.daily_ohlc.crossover_x_period_max(20, quote)
|
||||
breakthrough_20_low = self.daily_ohlc.crossunder_x_period_min(20, quote)
|
||||
breakthrough_55_high = self.daily_ohlc.crossover_x_period_max(55, quote)
|
||||
breakthrough_55_low = self.daily_ohlc.crossunder_x_period_min(55, quote)
|
||||
|
||||
if self.trade_next_20_day_breakout and breakthrough_20_high:
|
||||
self.__to_LONG_state(self.daily_ohlc.rolling_max(20))
|
||||
elif self.trade_next_20_day_breakout and breakthrough_20_low:
|
||||
self.__to_SHORT_state(self.daily_ohlc.rolling_min(20))
|
||||
elif breakthrough_55_high:
|
||||
self.__to_LONG_state(self.daily_ohlc.rolling_max(55))
|
||||
elif breakthrough_55_low:
|
||||
self.__to_SHORT_state(self.daily_ohlc.rolling_min(55))
|
||||
elif breakthrough_20_high:
|
||||
self.__to_SHADOW_LONG_state(self.daily_ohlc.rolling_max(20))
|
||||
elif breakthrough_20_low:
|
||||
self.__to_SHADOW_SHORT_state(self.daily_ohlc.rolling_min(20))
|
||||
|
||||
def __process_LONG_state(self, quote: Quote):
|
||||
exit_price = max(self.daily_ohlc.rolling_min(10), self.__last_order_price() - self.N * 2)
|
||||
next_entry_price = self.__last_order_price() + self.N / 2
|
||||
|
||||
if crossunder(quote, exit_price):
|
||||
self.trade_next_20_day_breakout = self.portfolio.unrealized_gains(self.instr, exit_price) < 0
|
||||
self.__to_INIT_state(exit_price)
|
||||
elif crossover(quote, next_entry_price) and not self.__reach_unit_limit():
|
||||
self.__submit_order(self.unit_size, next_entry_price)
|
||||
|
||||
def __process_SHORT_state(self, quote: Quote):
|
||||
exit_price = min(self.daily_ohlc.rolling_max(10), self.__last_order_price() + self.N * 2)
|
||||
next_entry_price = self.__last_order_price() - self.N / 2
|
||||
|
||||
if crossover(quote, exit_price):
|
||||
self.trade_next_20_day_breakout = self.portfolio.unrealized_gains(self.instr, exit_price) < 0
|
||||
self.__to_INIT_state(exit_price)
|
||||
elif crossunder(quote, next_entry_price) and not self.__reach_unit_limit():
|
||||
self.__submit_order(-self.unit_size, next_entry_price)
|
||||
|
||||
def __process_SHADOW_LONG_state(self, quote: Quote):
|
||||
exit_price = max(self.daily_ohlc.rolling_min(10), self.__shadow_last_order_price() - self.N * 2)
|
||||
next_entry_price = self.__shadow_last_order_price() + self.N / 2
|
||||
|
||||
if self.daily_ohlc.crossover_x_period_max(55, quote):
|
||||
self.__to_LONG_state(self.daily_ohlc.rolling_max(55))
|
||||
elif self.daily_ohlc.crossunder_x_period_min(55, quote):
|
||||
self.__to_SHORT_state(self.daily_ohlc.rolling_min(55))
|
||||
elif crossunder(quote, exit_price):
|
||||
shadow_unrealized_gains = self.shadow_portfolio.unrealized_gains(self.instr, exit_price)
|
||||
self.trade_next_20_day_breakout = shadow_unrealized_gains < 0
|
||||
self.__to_INIT_state(exit_price)
|
||||
elif crossover(quote, next_entry_price) and not self.__shadow_reach_unit_limit():
|
||||
self.__shadow_submit_order(self.unit_size, next_entry_price)
|
||||
|
||||
def __process_SHADOW_SHORT_state(self, quote: Quote):
|
||||
exit_price = min(self.daily_ohlc.rolling_max(10), self.__shadow_last_order_price() + self.N * 2)
|
||||
next_entry_price = self.__shadow_last_order_price() - self.N / 2
|
||||
|
||||
if self.daily_ohlc.crossover_x_period_max(55, quote):
|
||||
self.__to_LONG_state(self.daily_ohlc.rolling_max(55))
|
||||
elif self.daily_ohlc.crossunder_x_period_min(55, quote):
|
||||
self.__to_SHORT_state(self.daily_ohlc.rolling_min(55))
|
||||
elif crossover(quote, exit_price):
|
||||
shadow_unrealized_gains = self.shadow_portfolio.unrealized_gains(self.instr, exit_price)
|
||||
self.trade_next_20_day_breakout = shadow_unrealized_gains < 0
|
||||
self.__to_INIT_state(exit_price)
|
||||
elif crossunder(quote, next_entry_price) and not self.__shadow_reach_unit_limit():
|
||||
self.__shadow_submit_order(-self.unit_size, next_entry_price)
|
||||
|
||||
@override
|
||||
def unfilled_positions(self, instr: Instrument) -> Position:
|
||||
x = self.desired_portfolio.outstanding_shares(instr) - self.portfolio.outstanding_shares(instr)
|
||||
return self.desired_portfolio.consolidate_last_x_shares(instr, x)
|
||||
|
||||
@override
|
||||
def order_filled(self, new_pos: Position):
|
||||
self.portfolio.add_position(new_pos)
|
||||
|
||||
@override
|
||||
def process_bid_ask(self, bid_ask: BidAsk):
|
||||
assert False, 'todo'
|
||||
|
||||
@override
|
||||
def process_ohlc(self, ohlc: OHLC):
|
||||
prev_day_cnt = len(self.daily_ohlc)
|
||||
self.daily_ohlc.append(ohlc)
|
||||
curr_day_cnt = len(self.daily_ohlc)
|
||||
|
||||
if self.state == State.WARMUP:
|
||||
raise RuntimeError('strategy wasn\'t warmed up yet')
|
||||
|
||||
if prev_day_cnt != curr_day_cnt:
|
||||
self.N = (19 * self.N + self.__true_range(-1)) / 20
|
||||
|
||||
net_liq = self.net_liquid_value(ohlc.close)
|
||||
|
||||
while net_liq < self.tradable_acct_balance * 0.9:
|
||||
self.tradable_acct_balance *= 0.8
|
||||
self.__adjust_unit_size()
|
||||
|
||||
while net_liq > self.tradable_acct_balance / 0.8:
|
||||
self.tradable_acct_balance /= 0.8
|
||||
self.__adjust_unit_size()
|
||||
|
||||
match self.state:
|
||||
case State.INIT:
|
||||
self.__process_INIT_state(ohlc)
|
||||
case State.LONG:
|
||||
self.__process_LONG_state(ohlc)
|
||||
case State.SHORT:
|
||||
self.__process_SHORT_state(ohlc)
|
||||
case State.SHADOW_LONG:
|
||||
self.__process_SHADOW_LONG_state(ohlc)
|
||||
case State.SHADOW_SHORT:
|
||||
self.__process_SHADOW_SHORT_state(ohlc)
|
||||
case _:
|
||||
raise RuntimeError('invalid state')
|
||||
|
||||
@override
|
||||
def net_liquid_value(self, at_price: float) -> float:
|
||||
return self.init_balance + self.portfolio.total_gains(self.instr, at_price)
|
||||
Reference in New Issue
Block a user