218 lines
6.3 KiB
Python
218 lines
6.3 KiB
Python
# piker: trading gear for hackers
|
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
'''
|
|
CLI front end for trades ledger and position tracking management.
|
|
|
|
'''
|
|
from typing import (
|
|
Any,
|
|
)
|
|
|
|
from rich.console import Console
|
|
from rich.markdown import Markdown
|
|
import tractor
|
|
import trio
|
|
import typer
|
|
|
|
from ..service import (
|
|
open_piker_runtime,
|
|
)
|
|
from ..clearing._messages import BrokerdPosition
|
|
from ..calc import humanize
|
|
|
|
|
|
ledger = typer.Typer()
|
|
|
|
|
|
def broker_init(
|
|
brokername: str,
|
|
loglevel: str | None = None,
|
|
|
|
**start_actor_kwargs,
|
|
|
|
) -> dict:
|
|
'''
|
|
Given an input broker name, load all named arguments
|
|
which can be passed to a daemon + context spawn for
|
|
the relevant `brokerd` service endpoint.
|
|
|
|
'''
|
|
from ..brokers import get_brokermod
|
|
brokermod = get_brokermod(brokername)
|
|
modpath = brokermod.__name__
|
|
|
|
start_actor_kwargs['name'] = f'brokerd.{brokername}'
|
|
start_actor_kwargs.update(
|
|
getattr(
|
|
brokermod,
|
|
'_spawn_kwargs',
|
|
{},
|
|
)
|
|
)
|
|
|
|
# lookup actor-enabled modules declared by the backend offering the
|
|
# `brokerd` endpoint(s).
|
|
enabled = start_actor_kwargs['enable_modules'] = [modpath]
|
|
for submodname in getattr(
|
|
brokermod,
|
|
'__enable_modules__',
|
|
[],
|
|
):
|
|
subpath = f'{modpath}.{submodname}'
|
|
enabled.append(subpath)
|
|
|
|
# non-blocking setup of brokerd service nursery
|
|
from ..data import _setup_persistent_brokerd
|
|
|
|
return (
|
|
start_actor_kwargs, # to `ActorNursery.start_actor()`
|
|
_setup_persistent_brokerd, # service task ep
|
|
getattr( # trades endpoint
|
|
brokermod,
|
|
'trades_dialogue',
|
|
None,
|
|
),
|
|
)
|
|
|
|
|
|
@ledger.command()
|
|
def sync(
|
|
fully_qualified_account_name: str,
|
|
pdb: bool = False,
|
|
|
|
loglevel: str = typer.Option(
|
|
'error',
|
|
"-l",
|
|
),
|
|
):
|
|
console = Console()
|
|
|
|
try:
|
|
brokername, account = fully_qualified_account_name.split('.')
|
|
except ValueError:
|
|
md = Markdown(
|
|
f'=> `{fully_qualified_account_name}` <=\n\n'
|
|
'is not a valid '
|
|
'__fully qualified account name?__\n\n'
|
|
'Your account name needs to be of the form '
|
|
'`<brokername>.<account_name>`\n'
|
|
)
|
|
console.print(md)
|
|
return
|
|
|
|
start_kwargs, _, trades_ep = broker_init(
|
|
brokername,
|
|
loglevel=loglevel,
|
|
)
|
|
|
|
async def main():
|
|
|
|
async with (
|
|
open_piker_runtime(
|
|
name='ledger_cli',
|
|
loglevel=loglevel,
|
|
debug_mode=pdb,
|
|
|
|
) as (actor, sockaddr),
|
|
|
|
tractor.open_nursery() as an,
|
|
):
|
|
portal = await an.start_actor(
|
|
loglevel=loglevel,
|
|
debug_mode=pdb,
|
|
**start_kwargs,
|
|
)
|
|
|
|
if (
|
|
brokername == 'paper'
|
|
or trades_ep is None
|
|
):
|
|
from ..clearing import _paper_engine as paper
|
|
open_trades_endpoint = paper.open_paperboi(
|
|
fqme=None, # tell paper to not start clearing loop
|
|
broker=brokername,
|
|
loglevel=loglevel,
|
|
)
|
|
else:
|
|
# open live brokerd trades endpoint
|
|
open_trades_endpoint = portal.open_context(
|
|
trades_ep,
|
|
loglevel=loglevel,
|
|
)
|
|
|
|
positions: dict[str, Any]
|
|
accounts: list[str]
|
|
async with (
|
|
open_trades_endpoint as (
|
|
brokerd_ctx,
|
|
(positions, accounts,),
|
|
),
|
|
):
|
|
summary: str = (
|
|
'[dim underline]Piker Position Summary[/] '
|
|
f'[dim blue underline]{brokername}[/]'
|
|
'[dim].[/]'
|
|
f'[blue underline]{account}[/]'
|
|
f'[dim underline] -> total pps: [/]'
|
|
f'[green]{len(positions)}[/]\n'
|
|
)
|
|
for ppdict in positions:
|
|
ppmsg = BrokerdPosition(**ppdict)
|
|
size = ppmsg.size
|
|
if size:
|
|
ppu: float = round(
|
|
ppmsg.avg_price,
|
|
ndigits=2,
|
|
)
|
|
cost_basis: str = humanize(size * ppu)
|
|
h_size: str = humanize(size)
|
|
|
|
if size < 0:
|
|
pcolor = 'red'
|
|
else:
|
|
pcolor = 'green'
|
|
|
|
# sematic-highlight of fqme
|
|
fqme = ppmsg.symbol
|
|
tokens = fqme.split('.')
|
|
styled_fqme = f'[blue underline]{tokens[0]}[/]'
|
|
for tok in tokens[1:]:
|
|
styled_fqme += '[dim].[/]'
|
|
styled_fqme += f'[dim blue underline]{tok}[/]'
|
|
|
|
# TODO: instead display in a ``rich.Table``?
|
|
summary += (
|
|
styled_fqme +
|
|
'[dim]: [/]'
|
|
f'[{pcolor}]{h_size}[/]'
|
|
'[dim blue]u @[/]'
|
|
f'[{pcolor}]{ppu}[/]'
|
|
'[dim blue] = [/]'
|
|
f'[{pcolor}]$ {cost_basis}\n[/]'
|
|
)
|
|
|
|
console.print(summary)
|
|
await brokerd_ctx.cancel()
|
|
|
|
await portal.cancel_actor()
|
|
|
|
trio.run(main)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ledger()
|