Browse Source

Merge master in barthpaleologue

master
Barthélemy Paléologue 5 months ago
parent
commit
2fee3d29a8
  1. 1
      .vscode/settings.json
  2. 8
      auto_trading/errors.py
  3. 22
      auto_trading/interfaces.py
  4. 22
      auto_trading/orders.py
  5. 4
      auto_trading/ptf/in_memory.py
  6. 91
      auto_trading/strat/prop.py
  7. 59
      auto_trading/strat/yoyo.py
  8. 19
      main.py
  9. 2
      tests/orders/test_orders.py
  10. 68
      tests/strat/test_prop.py
  11. 32
      tests/strat/test_yoyo.py

1
.vscode/settings.json

@ -1,5 +1,4 @@
{
"python.testing.pytestPath": "./scripts/test.sh",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"testOnSave.enabled": true,

8
auto_trading/errors.py

@ -25,3 +25,11 @@ class OrderFails(OrderException):
class PTFException(TradingException):
"""An error occur inside the PTF."""
class StrategyError(TradingException):
"""An error linked to a strategy was encountered."""
class StrategyInitError(StrategyError):
"""An error occurred during the strategy initialization."""

22
auto_trading/interfaces.py

@ -6,9 +6,10 @@ import logging
from abc import ABC, abstractmethod, abstractproperty
from dataclasses import dataclass, field
from datetime import datetime
from pandas import DataFrame, Timedelta, Series # type: ignore
from typing import Dict, List, Optional, Any, Callable
from pandas import DataFrame, Timedelta, Series # type: ignore
from .errors import OrderException, UnknowOrder, PTFException
@ -28,6 +29,9 @@ class PTFState:
for stock_name, amount in self.stocks.items()
)
def to_dict(self):
return {"USD": self.balance, **self.stocks}
@dataclass
class Order:
@ -142,22 +146,25 @@ class Strategy(ABC):
class PTF(ABC):
"""Somethink that buy or sell stocks."""
executors: Dict[Any, Callable[[Any, Any], None]] = {}
orders_history: List[Order]
states_history: List[PTFState]
states_history: DataFrame
def __init__(self, skip_errors: bool = True, save_errors: bool = True):
"""Init the class.
Args:
skip_errors (bool, optional): Do we skip orders in failure ? Defaults to True.
save_errors (bool, optional): Do we save orders in failure in the orders_history ? Defaults to True.
skip_errors (bool, optional): Do we skip orders in failure ?
Defaults to True.
save_errors (bool, optional): Do we save orders in failure in the orders_history ?
Defaults to True.
"""
self.logger = logging.getLogger(self.__class__.__name__)
self.orders_history = []
self.states_history = []
self.states_history = DataFrame()
self.skip_errors = skip_errors
self.save_errors = save_errors
@ -192,7 +199,10 @@ class PTF(ABC):
order.successfull = False
if self.save_errors or order.successfull:
self.orders_history.append(order)
self.states_history.append(self.state)
# self.states_history.append(self.state.to_dict(), ignore_index=True)
self.states_history = self.states_history.append(
self.state.to_dict(), ignore_index=True
)
def _execute(self, order: Order) -> None:
"""Execute one order.

22
auto_trading/orders.py

@ -17,9 +17,18 @@ class Long(Order):
"""The amount in $"""
return self.amount * self.price
def __repr__(self) -> str:
def __str__(self) -> str:
return f"{self.__class__.__name__}({self.stock}: {self.amount_usd}$)"
def __eq__(self, other) -> bool:
if not isinstance(other, Long):
return False
return (
self.stock == other.stock
and self.amount == other.amount
and self.price == other.price
)
@dataclass
class Short(Order):
@ -34,5 +43,14 @@ class Short(Order):
"""The amount in $"""
return self.amount * self.price
def __repr__(self) -> str:
def __str__(self) -> str:
return f"{self.__class__.__name__}({self.stock}: {self.amount_usd}$)"
def __eq__(self, other) -> bool:
if not isinstance(other, Short):
return False
return (
self.stock == other.stock
and self.amount == other.amount
and self.price == other.price
)

4
auto_trading/ptf/in_memory.py

@ -1,4 +1,4 @@
from typing import Dict, Callable, List
from typing import Dict, Callable
from ..interfaces import PTF, PTFState
from ..orders import Long, Short
@ -33,7 +33,7 @@ class InMemoryPortfolio(PTF):
def execute_short(self, order: Short) -> None:
"""Sell actions."""
if self._stocks[order.stock] < order.amount:
if (self._stocks.get(order.stock) or 0) < order.amount:
raise OrderFails("Not enough stock.")
if self.change_rate.get(order.stock, 0) < order.price:
raise OrderFails("You shell it too high.")

91
auto_trading/strat/prop.py

@ -0,0 +1,91 @@
from typing import List, Dict, Union
import pandas as pd # type: ignore
from ..orders import Long, Short
from ..interfaces import Strategy, Order, PTFState, Indicator
from ..errors import StrategyInitError
class Prop(Strategy):
"""Proportional repartion from the indicator."""
def __init__(self, indicators: Dict[str, Indicator] = None, max_delta: float = 10):
"""Init the class.
Only have one indicator.
The max delta is the maximum difference between the actual and the desired state.
"""
if not indicators or len(indicators) != 1:
raise StrategyInitError(
"This strat must be initialized with one indicator."
)
super().__init__(indicators=indicators)
self.indicator = list(indicators.keys())[0]
self.max_delta = max_delta
def filter_order(self, order: Union[Long, Short]) -> bool:
"""Return if the order value is high enough."""
return order.amount_usd > self.max_delta
def execute(
self, data: pd.DataFrame, indicators_results: pd.DataFrame, state: PTFState
) -> List[Order]:
"""Just hold the value [to_hold].
Args:
data (DataFrame): The Data broker output.
For each time and each stock give (high, low, open, close).
indicators_results (DataFrame): Indicator-Stock valuated float.
For each indicator and each stock give -1 if realy bad and +1 if realy good.
Returns:
List[Order]: A list of orders to execute.
"""
orders: List[Union[Long, Short]] = []
conversion_rate = data.loc[data.index[-1][0]].close.to_dict()
results = indicators_results.T[self.indicator]
stock_to_sell = []
# Remove all action that the indicator place as negative.
for stock in results[results < 0].index:
order = Short(stock, state.stocks[stock], conversion_rate[stock])
if self.filter_order(order):
orders.append(order)
stock_to_sell.append(stock)
total_money = state.balance + sum(
conversion_rate[stock_name] * amount
for stock_name, amount in state.stocks.items()
if results[stock] > 0 or stock_name in stock_to_sell
)
# Desired state
desired_state_precent = results[results > 0] / results[results > 0].sum()
desired_state_dolards = total_money * desired_state_precent
# Create the new buy orders from with the delta between the actual and the desired state.
for stock, amount in desired_state_dolards.items():
if stock in state.stocks:
if amount > state.stocks[stock]:
orders.append(
Long(
stock,
amount / conversion_rate[stock] - state.stocks[stock],
conversion_rate[stock],
)
)
else:
orders.append(
Short(
stock,
state.stocks[stock] - amount / conversion_rate[stock],
conversion_rate[stock],
)
)
# Filter low orders
orders = list(filter(self.filter_order, orders))
return orders # type: ignore

59
auto_trading/strat/yoyo.py

@ -0,0 +1,59 @@
"""Yoyo strategy."""
from auto_trading.interfaces import Strategy, Order, PTFState
from auto_trading.orders import Long, Short
from typing import List, Dict, cast, Callable
import pandas as pd # type: ignore
def is_positive(number: float | None) -> bool:
"""Return True if the price is positive."""
return number is not None and number > 0
class Yoyo(Strategy):
"""A strat that buy a stock one time then sell it."""
def __init__(self, stock_name: str):
super().__init__()
self.stock_name = stock_name
def execute(
self,
data: pd.DataFrame,
indicators_results: pd.DataFrame,
ptf_state: PTFState,
) -> List[Order]:
try:
market_price = (
data.loc[data.index[-1][0]].close.to_dict().get(self.stock_name)
)
except:
return []
if not market_price:
return []
todo: Order
if self.as_stocks(ptf_state.stocks):
self.logger.info("sell")
todo = self.create_sell_order(ptf_state, market_price)
else:
self.logger.info("buy")
todo = self.create_buy_order(ptf_state, market_price)
return [todo]
def as_stocks(self, stocks: Dict[str, float]) -> bool:
"""Check if we currently have some stocks to sell."""
return is_positive(stocks.get(self.stock_name))
def create_sell_order(self, ptf: PTFState, current_price: float) -> Order:
"""Create a sell order."""
return Short(self.stock_name, ptf.stocks[self.stock_name], current_price)
def create_buy_order(self, ptf: PTFState, current_price: float) -> Order:
"""Create a buy order."""
return Long(self.stock_name, ptf.balance / current_price, current_price)

19
main.py

@ -3,12 +3,16 @@ import pandas as pd # type: ignore
import logging
from auto_trading.broker.backtest import Backtest
from auto_trading.indicators.ema2 import EMA
from auto_trading.indicators.sma2 import SMA
from auto_trading.strat.buyupselldown import BuyUpSellDown
from auto_trading.indicators.ema import EMA
from auto_trading.indicators.sma import SMA
from auto_trading.interfaces import Strategy
from auto_trading.orders import Long, Short
from auto_trading.strat.yoyo import Yoyo
from auto_trading.strat.hold import Hold
from auto_trading.ptf.in_memory import InMemoryPortfolio
from auto_trading.bot import Bot
from auto_trading.strat.prop import Prop
pd.options.plotting.backend = "plotly"
logging.basicConfig(level=logging.INFO)
@ -48,9 +52,10 @@ if __name__ == "__main__":
print(bot.ptf.total_balance(bot.broker.current_change))
# plot the history
ch_history: pd.DataFrame = bot.broker.change_rate_history # type: ignore
st_history = pd.DataFrame(
[s.stocks for s in bot.ptf.states_history],
index=ch_history.index,
)
ch_history["USD"] = 1
st_history = bot.ptf.states_history
st_history.index = ch_history.index
(st_history * ch_history).fillna(0).plot.area().show()

2
tests/orders/test_orders.py

@ -33,4 +33,4 @@ def test_order_usd(order: Union[Long, Short], usd: float):
)
def test_repr(order: Union[Long, Short], string: str):
"""Test repr of an order."""
assert repr(order) == string
assert str(order) == string

68
tests/strat/test_prop.py

@ -0,0 +1,68 @@
import pytest
from datetime import datetime
from pandas import DataFrame, Series # type: ignore
from auto_trading.strat.prop import Prop
from auto_trading.indicators.dumb import Dumb
from auto_trading.interfaces import PTFState
from auto_trading.orders import Long, Short
from auto_trading.errors import StrategyInitError
date = datetime.strptime("2015-03-31", "%Y-%m-%d")
@pytest.mark.parametrize(
"max_delta, data, indicators_results, state, results",
[
(
10,
DataFrame({"close": {(date, "GOOG"): 10, (date, "GOOGL"): 10}}),
Series({"GOOG": 1, "GOOGL": -1}),
PTFState(50, {"GOOG": 0, "GOOGL": 0}),
[Long("GOOG", price=10, amount=5)],
),
(
10,
DataFrame({"close": {(date, "GOOG"): 10, (date, "GOOGL"): 10}}),
Series({"GOOG": 1, "GOOGL": -1}),
PTFState(5, {"GOOG": 0, "GOOGL": 0}),
[],
),
(
1,
DataFrame({"close": {(date, "GOOG"): 10, (date, "GOOGL"): 10}}),
Series({"GOOG": 1, "GOOGL": -1}),
PTFState(0, {"GOOG": 0, "GOOGL": 1}),
[Short("GOOGL", price=10, amount=1), Long("GOOG", price=10, amount=1)],
),
(
20,
DataFrame(
{
"close": {
(date, "A"): 10,
(date, "B"): 10,
(date, "C"): 10,
(date, "D"): 10,
}
}
),
Series({"A": 1, "B": -1, "C": -1, "D": -1}),
PTFState(0, {"A": 0, "B": 1, "C": 1, "D": 1}),
[],
),
],
)
def test_prop(max_delta, data, indicators_results, state, results):
strat = Prop(max_delta=max_delta, indicators={"ind": Dumb(indicators_results)})
res = strat.run(data, state)
assert len(res) == len(results)
for result in results:
assert result in res
def test_not_multiples_indicators():
ind = Dumb(Series({"GOOG": 1, "GOOGL": -1}))
with pytest.raises(StrategyInitError):
Prop(indicators={"one": ind, "two": ind})

32
tests/strat/test_yoyo.py

@ -0,0 +1,32 @@
import pytest
from datetime import datetime
from pandas import DataFrame # type: ignore
from auto_trading.strat.yoyo import Yoyo
from auto_trading.interfaces import PTFState as State
from auto_trading.orders import Long, Short
date = datetime.strptime("2015-03-31", "%Y-%m-%d")
@pytest.mark.parametrize(
"data, state, output",
[
(DataFrame(), State(balance=0, stocks={}), None),
(DataFrame({"close": {(date, "AAPL"): 0}}), State(balance=0, stocks={}), None),
(
DataFrame({"close": {(date, "AAPL"): 10}}),
State(balance=0, stocks={"AAPL": 1}),
Short,
),
(DataFrame({"close": {(date, "AAPL"): 10}}), State(balance=10, stocks={}), Long),
],
)
def test_yoyo(data, state, output):
strat = Yoyo("AAPL")
res = strat.run(data, state)
if output is None:
assert len(res) == 0
else:
assert len(res) == 1
assert isinstance(res[0], output)
Loading…
Cancel
Save