191 lines
5.6 KiB
Python
191 lines
5.6 KiB
Python
# piker: trading gear for hackers
|
|
# Copyright (C) 2018-present Tyler Goodlet (in stewardship of 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/>.
|
|
|
|
"""
|
|
Time-series (remote) annotation APIs.
|
|
|
|
"""
|
|
from __future__ import annotations
|
|
from math import copysign
|
|
from typing import (
|
|
Any,
|
|
TYPE_CHECKING,
|
|
)
|
|
|
|
import polars as pl
|
|
import tractor
|
|
|
|
from piker.data._formatters import BGM
|
|
from piker.storage import log
|
|
|
|
if TYPE_CHECKING:
|
|
from piker.ui._remote_ctl import AnnotCtl
|
|
|
|
|
|
async def markup_gaps(
|
|
fqme: str,
|
|
timeframe: float,
|
|
actl: AnnotCtl,
|
|
wdts: pl.DataFrame,
|
|
gaps: pl.DataFrame,
|
|
|
|
) -> dict[int, dict]:
|
|
'''
|
|
Remote annotate time-gaps in a dt-fielded ts (normally OHLC)
|
|
with rectangles.
|
|
|
|
'''
|
|
aids: dict[int] = {}
|
|
for i in range(gaps.height):
|
|
row: pl.DataFrame = gaps[i]
|
|
|
|
# the gap's RIGHT-most bar's OPEN value
|
|
# at that time (sample) step.
|
|
iend: int = row['index'][0]
|
|
# dt: datetime = row['dt'][0]
|
|
# dt_prev: datetime = row['dt_prev'][0]
|
|
# dt_end_t: float = dt.timestamp()
|
|
|
|
|
|
# TODO: can we eventually remove this
|
|
# once we figure out why the epoch cols
|
|
# don't match?
|
|
# TODO: FIX HOW/WHY these aren't matching
|
|
# and are instead off by 4hours (EST
|
|
# vs. UTC?!?!)
|
|
# end_t: float = row['time']
|
|
# assert (
|
|
# dt.timestamp()
|
|
# ==
|
|
# end_t
|
|
# )
|
|
|
|
# the gap's LEFT-most bar's CLOSE value
|
|
# at that time (sample) step.
|
|
prev_r: pl.DataFrame = wdts.filter(
|
|
pl.col('index') == iend - 1
|
|
)
|
|
# XXX: probably a gap in the (newly sorted or de-duplicated)
|
|
# dt-df, so we might need to re-index first..
|
|
dt: pl.Series = row['dt']
|
|
dt_prev: pl.Series = row['dt_prev']
|
|
if prev_r.is_empty():
|
|
|
|
# XXX, filter out any special ignore cases,
|
|
# - UNIX-epoch stamped datums
|
|
# - first row
|
|
if (
|
|
dt_prev.dt.epoch()[0] == 0
|
|
or
|
|
dt.dt.epoch()[0] == 0
|
|
):
|
|
log.warning('Skipping row with UNIX epoch timestamp ??')
|
|
continue
|
|
|
|
if wdts[0]['index'][0] == iend: # first row
|
|
log.warning('Skipping first-row (has no previous obvi) !!')
|
|
continue
|
|
|
|
# XXX, if the previous-row by shm-index is missing,
|
|
# meaning there is a missing sample (set), get the prior
|
|
# row by df index and attempt to use it?
|
|
i_wdts: pl.DataFrame = wdts.with_row_index(name='i')
|
|
i_row: int = i_wdts.filter(pl.col('index') == iend)['i'][0]
|
|
prev_row_by_i = wdts[i_row]
|
|
prev_r: pl.DataFrame = prev_row_by_i
|
|
|
|
# debug any missing pre-row
|
|
if tractor._state.is_debug_mode():
|
|
await tractor.pause()
|
|
|
|
istart: int = prev_r['index'][0]
|
|
# TODO: implement px-col width measure
|
|
# and ensure at least as many px-cols
|
|
# shown per rect as configured by user.
|
|
# gap_w: float = abs((iend - istart))
|
|
# if gap_w < 6:
|
|
# margin: float = 6
|
|
# iend += margin
|
|
# istart -= margin
|
|
|
|
opn: float = row['open'][0]
|
|
cls: float = prev_r['close'][0]
|
|
|
|
# BGM=0.16 is the normal diff from overlap between bars, SO
|
|
# just go slightly "in" from that "between them".
|
|
from_idx: int = BGM - .06 # = .10
|
|
lc: tuple[float, float] = (
|
|
istart + 1 - from_idx,
|
|
cls,
|
|
)
|
|
ro: tuple[float, float] = (
|
|
iend + from_idx,
|
|
opn,
|
|
)
|
|
|
|
diff: float = cls - opn
|
|
sgn: float = copysign(1, diff)
|
|
color: str = 'dad_blue'
|
|
# TODO? mks more sense to have up/down coloring?
|
|
# color: str = {
|
|
# -1: 'lilypad_green', # up-gap
|
|
# 1: 'wine', # down-gap
|
|
# }[sgn]
|
|
|
|
rect_kwargs: dict[str, Any] = dict(
|
|
fqme=fqme,
|
|
timeframe=timeframe,
|
|
start_pos=lc,
|
|
end_pos=ro,
|
|
color=color,
|
|
)
|
|
|
|
# add up/down rects
|
|
aid: int = await actl.add_rect(**rect_kwargs)
|
|
assert aid
|
|
aids[aid] = rect_kwargs
|
|
direction: str = (
|
|
'down' if sgn == 1
|
|
else 'up'
|
|
)
|
|
# TODO! mk this a `msgspec.Struct` which we deserialize
|
|
# on the server side!
|
|
arrow_kwargs: dict[str, Any] = dict(
|
|
fqme=fqme,
|
|
timeframe=timeframe,
|
|
x=iend,
|
|
y=cls,
|
|
color=color,
|
|
alpha=169,
|
|
pointing=direction,
|
|
# TODO: expose these as params to markup_gaps()?
|
|
headLen=10,
|
|
headWidth=2.222,
|
|
pxMode=True,
|
|
)
|
|
|
|
aid: int = await actl.add_arrow(
|
|
**arrow_kwargs
|
|
)
|
|
|
|
# tell chart to redraw all its
|
|
# graphics view layers Bo
|
|
await actl.redraw(
|
|
fqme=fqme,
|
|
timeframe=timeframe,
|
|
)
|
|
return aids
|