Skip to content

Commit

Permalink
Fix incomplete functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
PoorRican committed Nov 15, 2022
1 parent c52dc8b commit 54eca04
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 47 deletions.
51 changes: 28 additions & 23 deletions analysis/financials.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC
import logging
import pandas as pd
from typing import NoReturn, Union
from typing import NoReturn
from warnings import warn

from strategies.strategy import Strategy
Expand Down Expand Up @@ -91,21 +92,27 @@ def _remaining(self) -> int:
return self.order_count - len(self.incomplete)

@property
def starting(self) -> Union[float, None]:
""" Amount of capitol to use for a trade.
def starting(self) -> float:
""" Amount of capitol to use for a buying assets.
Value is computed dynamically so that trades grow larger as the amount of available capitol increases. A
fraction of available capitol is used instead of spending all available to protect against trading inactivity
during a price drop.
Notes:
Since the returned value is used when buying assets, a warning is raised and logged when `_remaining` equals
to 0.
This fraction is dynamically calculated due to the assumption that order might remain incomplete. Flexibility
is desired when placing a new order.
Returns:
Amount of capitol to use when beginning to calculate final trade amount. `None` is returned where there
`_remaining` returns 0 and therefore no more trades should be executed.
Amount of capitol to use when beginning to calculate final trade amount.
"""
_remaining = self._remaining
if _remaining:
return self.capitol / self._remaining
if self._remaining == 0:
msg = '`starting` accessed while buying is restricted (`_remaining` == 0)'
warn(msg)
logging.warning(msg)

return None
return self.capitol / self.order_count

def pnl(self) -> float:
# TODO: `unpaired_buys` need to be reflected. Either buy including current price, or excluding and mentioning
Expand Down Expand Up @@ -148,7 +155,7 @@ def _adjust_assets(self, trade: SuccessfulTrade) -> NoReturn:
logging.warning(msg)

def unpaired(self) -> pd.DataFrame:
""" Select of unpaired orders by cross-referencing `unpaired_buys`.
""" Select of unpaired orders by cross-referencing `incomplete`.
This retrieves orders data whose assets have not been sold yet. Used during calculation of pnl, and during
normal trading operation to attempt sale of unsold assets and to clear `incomplete` when assets are sold.
Expand All @@ -163,7 +170,7 @@ def _check_unpaired(self, rate: float, original: bool = True) -> pd.DataFrame:
Args:
rate:
Select rows whose rate is <= given `rate`. Since buy orders cannot be sold at a rate lower than the
Select rows whose rate is <= given `rate`. Since buy orders should be sold at a rate lower than the
buy price, rate is used to select incomplete order rows when determining amount of available assets
should be sold.
original:
Expand Down Expand Up @@ -213,11 +220,11 @@ def _handle_inactive(self, row: pd.Series) -> NoReturn:
assert type(row) is pd.Series
assert row['side'] == Side.BUY

if row['id'] in self.incomplete['id']:
if row['id'] in self.incomplete['id'].values:
warn('Adding duplicate id found in `incomplete`')

_row = pd.DataFrame([[row['amt'], row['rate'], row['id']]], columns=['amt', 'rate', 'id'])
self.incomplete = pd.concat([self.incomplete, row],
self.incomplete = pd.concat([self.incomplete, _row],
ignore_index=True)

def _clean_incomplete(self, trade: SuccessfulTrade):
Expand Down Expand Up @@ -270,23 +277,21 @@ def _deduct_sold(self, trade: SuccessfulTrade, unpaired: pd.DataFrame) -> NoRetu
TODO:
- Track related orders
"""

# deduct from oldest unpaired order
unpaired.sort_index(inplace=True)
assert trade.side == Side.SELL

# account for assets acquired from previous buy
last_buy = self.orders[self.orders['side'] == Side.BUY].iloc[-1]
amt = trade.amt - last_buy['amt']
excess = trade.amt - last_buy['amt']

# deduct excess from unpaired orders
_drop = [] # rows to drop
for index, order in unpaired.iterrows():
if amt >= order['amt']:
_drop.append(order.name)
amt -= order['amt']
if excess >= order['amt']:
_drop.append(index)
excess -= order['amt']
else:
# update amount remaining.
_remaining = order['amt'] - amt
_remaining = order['amt'] - excess
self.incomplete.loc[self.incomplete['id'] == order['id'], 'amt'] = _remaining

self.incomplete.drop(index=_drop, inplace=True)
6 changes: 3 additions & 3 deletions analysis/trend.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ def characterize(self, point: Optional[pd.Timestamp] = None) -> MarketTrend:

# remove `freq` value to prevent `KeyError`
# TODO: is reusing the name going to affect original `point`?
point = pd.Timestamp.fromtimestamp(point.timestamp(), tz=timezone('US/Pacific'))
_point = pd.Timestamp.fromtimestamp(point.timestamp(), tz=timezone('US/Pacific'))

results = self._fetch_trends(point)
results = self._fetch_trends(_point)
consensus = self._determine_consensus(list(results.values()))

return MarketTrend(consensus, scalar=self._determine_scalar())
Expand All @@ -155,7 +155,7 @@ def _determine_consensus(values: Sequence['TrendMovement']) -> TrendMovement:
""" Return the most common value in a dict containing a list of returned results """
counts = {}
for v in TrendMovement.__members__.values():
counts[v] = list(values).count(v)
counts[v] = values.count(v)
_max = max(counts.values())
i = list(counts.values()).index(_max)
winner = list(counts.keys())[i]
Expand Down
19 changes: 14 additions & 5 deletions strategies/OscillatingStrategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,20 @@ def _oscillation(self, signal: Signal, timeout=True) -> bool:
Returns:
`true` if `signal` decision values.
"""
if self.orders.empty:
# force buy for first trade
if self.orders.empty: # first trade must be buy
# TODO: check if `assets` is 0
return signal == Signal.BUY

if signal:
last_order = self.orders.iloc[-1]
if last_order['side'] == signal == Signal.BUY and self._remaining and timeout:
# Allow repeated buys on timeout

# prevent more buy orders when there are too many incomplete orders
if self._remaining == 0 and signal == Signal.BUY:
return False
# Allow repeated buys on timeout
elif last_order['side'] == signal == Signal.BUY and self._remaining and timeout:
inactive = self._check_timeout()
if inactive:
# Add repeated buy to `unpaired_buys`
self._handle_inactive(last_order)
return inactive
return last_order.side != signal
Expand All @@ -57,12 +60,18 @@ def _determine_position(self, point: pd.Timestamp = None) -> Union[Tuple[Side, '
Oscillation of trade types is executed here. Duplicate trade type is not returned if a new signal is
generated.
Number of incomplete (outstanding) orders is limited here. If there are no remaining allowed orders
(as defined by `_remaining`) then False is returned.
Notes:
`self.indicators.develop()` needs to be called beforehand.
"""
if not point:
point = self.market.data.iloc[-1].name

if self._remaining <= 1:
pass

signal: Signal = self.indicators.check(self.market.data, point)
if self._oscillation(signal):
signal: Side = Side(signal)
Expand Down
10 changes: 0 additions & 10 deletions strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,3 @@
To determine the fitness and performance of the trading strategy, reporting functions can the total amount of
assets and fiat accrued. This can be used in active implementations as well as during backtesting.
"""

import strategies.strategy
import strategies.StaticAlternatingStrategy
import strategies.OscillatingStrategy
import strategies.ThreeProngAlt

from strategies.strategy import Strategy
from strategies.StaticAlternatingStrategy import StaticAlternatingStrategy
from strategies.OscillatingStrategy import OscillatingStrategy
from strategies.ThreeProngAlt import ThreeProngAlt
24 changes: 18 additions & 6 deletions tests/test_analysis_financials.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,35 @@ def test_post_sale(self):
def test_handle_inactive(self):
self.assertTrue(self.strategy.incomplete.empty)

row = pd.DataFrame({'amt': [5], 'rate': [5], 'id': [5]})
row = pd.Series({'amt': 5, 'rate': 5, 'id': 5, 'side': Side.BUY})

self.strategy._handle_inactive(row)

self.assertTrue(row['id'].isin(self.strategy.incomplete['id'])[0])
self.assertTrue(row['id'] in self.strategy.incomplete['id'].values)

# check that values remain after second addition
row2 = pd.DataFrame({'amt': [2], 'rate': [2], 'id': [2]})
row2 = pd.Series({'amt': 2, 'rate': 2, 'id': 2, 'side': Side.BUY})
self.strategy._handle_inactive(row2)

self.assertTrue(row['id'].isin(self.strategy.incomplete['id'])[0])
self.assertTrue(row2['id'].isin(self.strategy.incomplete['id'])[0])
self.assertTrue(row['id'] in self.strategy.incomplete['id'].values)
self.assertTrue(row2['id'] in self.strategy.incomplete['id'].values)

self.assertEqual(len(self.strategy.incomplete), 2)

# ============================== #
# assert exceptions and warnings #

# side must be `BUY
with self.assertRaises(AssertionError):
invalid_row = pd.Series({'amt': 5, 'rate': 5, 'id': 5, 'side': Side.SELL})
self.strategy._handle_inactive(invalid_row)

# row must be `Series`
with self.assertRaises(AssertionError):
self.strategy._handle_inactive(pd.concat([row, row]))
# noinspection PyTypeChecker
self.strategy._handle_inactive(pd.DataFrame())

# warn upon adding duplicates
with self.assertWarns(Warning):
self.strategy._handle_inactive(row)

Expand Down

0 comments on commit 54eca04

Please sign in to comment.