Guess exit transaction costs for BEP prediction

In order to attempt giving the user a realistic prediction for a BEP per
txn we need to model what the (worst case) anticipated exit txn costs
will be during the equivalent, paired entries. For now we use a simple
"symmetric cost prediction" model where we assume the exit costs will be
simply the same as the enter txn costs and thus on every entry we apply
2x the enter txn cost; on exit txns we then unroll these predictions by
keeping a cumulative sum of the cost-per-unit and reversing the charges
based on applying that mean to the current exit txn's size. Once
unrolled we apply the actual exit txn cost received from the
broker-provider.
account_tests
Tyler Goodlet 2023-08-02 17:25:23 -04:00
parent 1e3a4ca36d
commit 7ecf2bd89a
1 changed files with 71 additions and 22 deletions

View File

@ -392,12 +392,6 @@ def open_ledger_dfs(
def ledger_to_dfs( def ledger_to_dfs(
ledger: TransactionLedger, ledger: TransactionLedger,
# include transaction cost in breakeven price
# and presume the worst case of the same cost
# to exit this transaction (even though in reality
# it will be dynamic based on exit stratetgy).
cost_scalar: float = 1,
) -> dict[str, pl.DataFrame]: ) -> dict[str, pl.DataFrame]:
txns: dict[str, Transaction] = ledger.to_txns() txns: dict[str, Transaction] = ledger.to_txns()
@ -471,7 +465,7 @@ def ledger_to_dfs(
( (
(pl.col('price') * pl.col('size')) (pl.col('price') * pl.col('size'))
+ +
pl.col('cost') (pl.col('cost')) # * pl.col('size').sign())
).alias('dst_bot'), ).alias('dst_bot'),
]).with_columns([ ]).with_columns([
@ -499,8 +493,10 @@ def ledger_to_dfs(
]).with_columns([ ]).with_columns([
pl.lit(0, dtype=pl.Utf8).alias('virt_cost'),
pl.lit(0, dtype=pl.Float64).alias('applied_cost'),
pl.lit(0, dtype=pl.Float64).alias('pos_ppu'), pl.lit(0, dtype=pl.Float64).alias('pos_ppu'),
pl.lit(0, dtype=pl.Float64).alias('per_exit_pnl'), pl.lit(0, dtype=pl.Float64).alias('per_txn_pnl'),
pl.lit(0, dtype=pl.Float64).alias('cum_pos_pnl'), pl.lit(0, dtype=pl.Float64).alias('cum_pos_pnl'),
pl.lit(0, dtype=pl.Float64).alias('pos_bep'), pl.lit(0, dtype=pl.Float64).alias('pos_bep'),
pl.lit(0, dtype=pl.Float64).alias('cum_ledger_pnl'), pl.lit(0, dtype=pl.Float64).alias('cum_ledger_pnl'),
@ -510,7 +506,7 @@ def ledger_to_dfs(
# could try using embedded lists to track which txns # could try using embedded lists to track which txns
# are part of which ppu / bep calcs? Not sure this will # are part of which ppu / bep calcs? Not sure this will
# look any better nor be any more performant though xD # look any better nor be any more performant though xD
# pl.lit([[0]], dtype=pl.List).alias('list'), # pl.lit([[0]], dtype=pl.List(pl.Float64)).alias('list'),
# choose fields to emit for accounting puposes # choose fields to emit for accounting puposes
]).select([ ]).select([
@ -528,7 +524,7 @@ def ledger_to_dfs(
last_cumsize: float = 0 last_cumsize: float = 0
last_ledger_pnl: float = 0 last_ledger_pnl: float = 0
last_pos_pnl: float = 0 last_pos_pnl: float = 0
# last_is_enter: bool = False # TODO: drop right? virt_costs: list[float, float] = [0., 0.]
# imperatively compute the PPU (price per unit) and BEP # imperatively compute the PPU (price per unit) and BEP
# (break even price) iteratively over the ledger, oriented # (break even price) iteratively over the ledger, oriented
@ -541,16 +537,17 @@ def ledger_to_dfs(
price: float = row['price'] price: float = row['price']
size: float = row['size'] size: float = row['size']
# the profit is ALWAYS decreased, aka made a "loss"
# by the constant fee charged by the txn provider!
# see below in final PnL calculation and row element
# set.
txn_cost: float = row['cost']
pnl: float = 0
# ALWAYS reset per-position cum PnL # ALWAYS reset per-position cum PnL
if last_cumsize == 0: if last_cumsize == 0:
last_pos_pnl: float = 0 last_pos_pnl: float = 0
# the profit is ALWAYS decreased, aka made a "loss"
# by the constant fee charged by the txn provider!
# TODO: support exit txn virtual cost which we
# resolve on exit txns incrementally?
pnl: float = -1 * row['cost']
# a "position size INCREASING" or ENTER transaction # a "position size INCREASING" or ENTER transaction
# which "makes larger", in src asset unit terms, the # which "makes larger", in src asset unit terms, the
# trade's side-size of the destination asset: # trade's side-size of the destination asset:
@ -558,6 +555,29 @@ def ledger_to_dfs(
# - "selling" (more short) units of the dst asset # - "selling" (more short) units of the dst asset
if is_enter: if is_enter:
# Naively include transaction cost in breakeven
# price and presume the worst case of the
# exact-same-cost-to-exit this transaction's worth
# of size even though in reality it will be dynamic
# based on exit strategy, price, liquidity, etc..
virt_cost: float = txn_cost
# cpu: float = cost / size
# cummean of the cost-per-unit used for modelling
# a projected future exit cost which we immediately
# include in the costs incorporated to BEP on enters
last_cum_costs_size, last_cpu = virt_costs
cum_costs_size: float = last_cum_costs_size + abs(size)
cumcpu = (
(last_cpu * last_cum_costs_size)
+
txn_cost
) / cum_costs_size
virt_costs = [cum_costs_size, cumcpu]
txn_cost = txn_cost + virt_cost
df[i, 'virt_cost'] = f'{-virt_cost} FROM {cumcpu}@{cum_costs_size}'
# a cumulative mean of the price-per-unit acquired # a cumulative mean of the price-per-unit acquired
# in the destination asset: # in the destination asset:
# https://en.wikipedia.org/wiki/Moving_average#Cumulative_average # https://en.wikipedia.org/wiki/Moving_average#Cumulative_average
@ -587,16 +607,44 @@ def ledger_to_dfs(
# only changes on position size increasing txns # only changes on position size increasing txns
ppu: float = last_ppu ppu: float = last_ppu
# include the per-txn profit or loss given we are # UNWIND IMPLIED COSTS FROM ENTRIES
# "closing" the position with this txn. # => Reverse the virtual/modelled (2x predicted) txn
pnl += (last_ppu - price) * size # cost that was included in the least-recently
# entered txn that is still part of the current CSi
# set.
# => we look up the cost-per-unit cumsum and apply
# if over the current txn size (by multiplication)
# and then reverse that previusly applied cost on
# the txn_cost for this record.
#
# NOTE: current "model" is just to previously assumed 2x
# the txn cost for a matching enter-txn's
# cost-per-unit; we then immediately reverse this
# prediction and apply the real cost received here.
last_cum_costs_size, last_cpu = virt_costs
prev_virt_cost: float = last_cpu * abs(size)
txn_cost: float = txn_cost - prev_virt_cost # +ve thus a "reversal"
cum_costs_size: float = last_cum_costs_size - abs(size)
virt_costs = [cum_costs_size, last_cpu]
df[i, 'virt_cost'] = (
f'{-prev_virt_cost} FROM {last_cpu}@{cum_costs_size}'
)
# the per-txn profit or loss (PnL) given we are
# (partially) "closing"/"exiting" the position via
# this txn.
pnl: float = (last_ppu - price) * size
# always subtract txn cost from total txn pnl
txn_pnl: float = pnl - txn_cost
# cumulative PnLs per txn # cumulative PnLs per txn
last_ledger_pnl = ( last_ledger_pnl = (
last_ledger_pnl + pnl last_ledger_pnl + txn_pnl
) )
last_pos_pnl = df[i, 'cum_pos_pnl'] = ( last_pos_pnl = df[i, 'cum_pos_pnl'] = (
last_pos_pnl + pnl last_pos_pnl + txn_pnl
) )
if cumsize == 0: if cumsize == 0:
@ -638,7 +686,8 @@ def ledger_to_dfs(
# inject DF row with all values # inject DF row with all values
df[i, 'pos_ppu'] = ppu df[i, 'pos_ppu'] = ppu
df[i, 'per_exit_pnl'] = pnl df[i, 'per_txn_pnl'] = txn_pnl
df[i, 'applied_cost'] = -txn_cost
df[i, 'cum_pos_pnl'] = last_pos_pnl df[i, 'cum_pos_pnl'] = last_pos_pnl
df[i, 'pos_bep'] = pos_bep df[i, 'pos_bep'] = pos_bep
df[i, 'cum_ledger_pnl'] = last_ledger_pnl df[i, 'cum_ledger_pnl'] = last_ledger_pnl