Compare commits
66 Commits
Author | SHA1 | Date |
---|---|---|
wattygetlood | 75faf83004 | |
wattygetlood | a270b2e033 | |
wattygetlood | 09fd8ef742 | |
wattygetlood | 422f203fc3 | |
wattygetlood | 8fbd0cd067 | |
wattygetlood | 68ed1164a1 | |
wattygetlood | 2f73f809f1 | |
wattygetlood | ba7b01b704 | |
Tyler Goodlet | 83299c3a8b | |
Tyler Goodlet | 0d17f4ff4c | |
Tyler Goodlet | 30dfb8f03d | |
Tyler Goodlet | 5f45404efb | |
Tyler Goodlet | 49885ca750 | |
Tyler Goodlet | 5b703782fc | |
Tyler Goodlet | 8bf9ebc55c | |
Tyler Goodlet | 6f2c2b46d5 | |
Tyler Goodlet | 65ad18b5c3 | |
Tyler Goodlet | 59defa378c | |
Tyler Goodlet | 65bf5a386d | |
Tyler Goodlet | aa3bff704c | |
Tyler Goodlet | 1061436c20 | |
Tyler Goodlet | 490126c672 | |
Tyler Goodlet | eb05c78381 | |
Tyler Goodlet | c677ff47a4 | |
Tyler Goodlet | bc2791526f | |
Tyler Goodlet | 25e2d8a28e | |
Tyler Goodlet | 11d18e2e8d | |
Tyler Goodlet | c85a50289e | |
wattygetlood | 2615af3955 | |
Tyler Goodlet | 8a151ddd54 | |
Tyler Goodlet | 6bea1b1adf | |
Tyler Goodlet | 65a645bdde | |
Tyler Goodlet | 936171a7d0 | |
Tyler Goodlet | 81666fd76e | |
Tyler Goodlet | 436a86ba2d | |
Tyler Goodlet | 9df27931ab | |
Tyler Goodlet | c440ecefa4 | |
Tyler Goodlet | 622372a7d5 | |
Tyler Goodlet | e367ffa107 | |
Tyler Goodlet | 590e08a4d4 | |
Tyler Goodlet | f98733118b | |
Tyler Goodlet | 3095d602e4 | |
Tyler Goodlet | d1a3c80d93 | |
Tyler Goodlet | b4cdd36337 | |
Tyler Goodlet | 280dfe36ca | |
Tyler Goodlet | 03ac7d075f | |
Tyler Goodlet | 9b7c8ed01b | |
Tyler Goodlet | 0a4bc72341 | |
Tyler Goodlet | 4621d1af1a | |
Tyler Goodlet | a042c7f2b3 | |
Tyler Goodlet | 1b437f80e0 | |
Tyler Goodlet | 92d6eedbb7 | |
Tyler Goodlet | 33e5a6628d | |
Tyler Goodlet | 6affead81e | |
Tyler Goodlet | fa9eebab35 | |
Tyler Goodlet | 4436ed2c18 | |
Tyler Goodlet | bf387bdc77 | |
Tyler Goodlet | c9359f265d | |
Tyler Goodlet | 11c5264025 | |
Tyler Goodlet | 52e89ecd67 | |
Tyler Goodlet | 99ac72455f | |
Tyler Goodlet | 0a7863e4fa | |
Tyler Goodlet | 46c9943612 | |
Tyler Goodlet | e8e5f1525c | |
Tyler Goodlet | f4e1362792 | |
Tyler Goodlet | 69c2dd866e |
28
README.rst
28
README.rst
|
@ -72,6 +72,34 @@ for a development install::
|
||||||
pip install -r requirements.txt -e .
|
pip install -r requirements.txt -e .
|
||||||
|
|
||||||
|
|
||||||
|
install for tinas
|
||||||
|
*****************
|
||||||
|
for windows peeps you can start by getting `conda installed`_
|
||||||
|
and the `C++ build toolz`_ on your system.
|
||||||
|
|
||||||
|
then, `crack a conda shell`_ and run the following commands::
|
||||||
|
|
||||||
|
conda create piker --python=3.9
|
||||||
|
conda activate piker
|
||||||
|
conda install pip
|
||||||
|
pip install --upgrade setuptools
|
||||||
|
cd dIreCToRieZ\oF\cODez\piker\
|
||||||
|
pip install -r requirements -e .
|
||||||
|
|
||||||
|
|
||||||
|
in order to look coolio in front of all ur tina friends (and maybe
|
||||||
|
want to help us with testin, hackzing or configgin), install
|
||||||
|
`vscode`_ and `setup a coolio tiled wm console`_ so you can start
|
||||||
|
living the life of the tech literate..
|
||||||
|
|
||||||
|
.. _conda installed: https://
|
||||||
|
.. _C++ build toolz: https://
|
||||||
|
.. _crack a conda shell: https://
|
||||||
|
.. _vscode: https://
|
||||||
|
|
||||||
|
.. link to the tina guide
|
||||||
|
.. _setup a coolio tiled wm console: https://
|
||||||
|
|
||||||
provider support
|
provider support
|
||||||
****************
|
****************
|
||||||
for live data feeds the in-progress set of supported brokers is:
|
for live data feeds the in-progress set of supported brokers is:
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
Notes to self
|
||||||
|
=============
|
||||||
|
chicken scratch we shan't forget, consider this staging
|
||||||
|
for actual feature issues on wtv git wrapper-provider we're
|
||||||
|
using (no we shan't stick with GH long term likely).
|
||||||
|
|
||||||
|
|
||||||
|
cool chart features
|
||||||
|
-------------------
|
||||||
|
- allow right-click to spawn shell with current in view
|
||||||
|
data passed to the new process via ``msgpack-numpy``.
|
||||||
|
- expand OHLC datum to lower time frame.
|
||||||
|
- auto-highlight current time range on tick feed
|
||||||
|
|
||||||
|
|
||||||
|
features from IB charting
|
||||||
|
-------------------------
|
||||||
|
- vlm diffing from ticks and compare when bar arrives from historical
|
||||||
|
- should help isolate dark vlm / trades
|
||||||
|
|
||||||
|
|
||||||
|
chart ux ideas
|
||||||
|
--------------
|
||||||
|
- hotkey to zoom to order intersection (horizontal line) with previous
|
||||||
|
price levels (+ some margin obvs).
|
||||||
|
- L1 "lines" (queue size repr) should normalize to some fixed x width
|
||||||
|
such that when levels with more vlm appear other smaller levels are
|
||||||
|
scaled down giving an immediate indication of the liquidity diff.
|
|
@ -18,30 +18,18 @@
|
||||||
Cacheing apis and toolz.
|
Cacheing apis and toolz.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# further examples of interest:
|
|
||||||
# https://gist.github.com/njsmith/cf6fc0a97f53865f2c671659c88c1798#file-cache-py-L8
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Hashable,
|
|
||||||
Optional,
|
|
||||||
TypeVar,
|
|
||||||
AsyncContextManager,
|
|
||||||
)
|
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
asynccontextmanager,
|
asynccontextmanager,
|
||||||
)
|
)
|
||||||
|
|
||||||
import trio
|
from tractor.trionics import maybe_open_context
|
||||||
from trio_typing import TaskStatus
|
|
||||||
import tractor
|
|
||||||
|
|
||||||
from .brokers import get_brokermod
|
from .brokers import get_brokermod
|
||||||
from .log import get_logger
|
from .log import get_logger
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,112 +62,6 @@ def async_lifo_cache(maxsize=128):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
_cache: dict[str, 'Client'] = {} # noqa
|
|
||||||
|
|
||||||
|
|
||||||
class cache:
|
|
||||||
'''Globally (processs wide) cached, task access to a
|
|
||||||
kept-alive-while-in-use async resource.
|
|
||||||
|
|
||||||
'''
|
|
||||||
lock = trio.Lock()
|
|
||||||
users: int = 0
|
|
||||||
values: dict[Any, Any] = {}
|
|
||||||
resources: dict[
|
|
||||||
int,
|
|
||||||
Optional[tuple[trio.Nursery, trio.Event]]
|
|
||||||
] = {}
|
|
||||||
no_more_users: Optional[trio.Event] = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def run_ctx(
|
|
||||||
cls,
|
|
||||||
mng,
|
|
||||||
key,
|
|
||||||
task_status: TaskStatus[T] = trio.TASK_STATUS_IGNORED,
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
async with mng as value:
|
|
||||||
|
|
||||||
_, no_more_users = cls.resources[id(mng)]
|
|
||||||
cls.values[key] = value
|
|
||||||
task_status.started(value)
|
|
||||||
try:
|
|
||||||
await no_more_users.wait()
|
|
||||||
finally:
|
|
||||||
value = cls.values.pop(key)
|
|
||||||
# discard nursery ref so it won't be re-used (an error)
|
|
||||||
cls.resources.pop(id(mng))
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def maybe_open_ctx(
|
|
||||||
|
|
||||||
key: Hashable,
|
|
||||||
mngr: AsyncContextManager[T],
|
|
||||||
|
|
||||||
) -> (bool, T):
|
|
||||||
'''Maybe open a context manager if there is not already a cached
|
|
||||||
version for the provided ``key``. Return the cached instance on
|
|
||||||
a cache hit.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
await cache.lock.acquire()
|
|
||||||
|
|
||||||
ctx_key = id(mngr)
|
|
||||||
|
|
||||||
value = None
|
|
||||||
try:
|
|
||||||
# lock feed acquisition around task racing / ``trio``'s
|
|
||||||
# scheduler protocol
|
|
||||||
value = cache.values[key]
|
|
||||||
log.info(f'Reusing cached resource for {key}')
|
|
||||||
cache.users += 1
|
|
||||||
cache.lock.release()
|
|
||||||
yield True, value
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
log.info(f'Allocating new resource for {key}')
|
|
||||||
|
|
||||||
# **critical section** that should prevent other tasks from
|
|
||||||
# checking the cache until complete otherwise the scheduler
|
|
||||||
# may switch and by accident we create more then one feed.
|
|
||||||
|
|
||||||
# TODO: avoid pulling from ``tractor`` internals and
|
|
||||||
# instead offer a "root nursery" in piker actors?
|
|
||||||
service_n = tractor.current_actor()._service_n
|
|
||||||
|
|
||||||
# TODO: does this need to be a tractor "root nursery"?
|
|
||||||
ln = cache.resources.get(ctx_key)
|
|
||||||
assert not ln
|
|
||||||
|
|
||||||
ln, _ = cache.resources[ctx_key] = (service_n, trio.Event())
|
|
||||||
|
|
||||||
value = await ln.start(cache.run_ctx, mngr, key)
|
|
||||||
cache.users += 1
|
|
||||||
cache.lock.release()
|
|
||||||
|
|
||||||
yield False, value
|
|
||||||
|
|
||||||
finally:
|
|
||||||
cache.users -= 1
|
|
||||||
|
|
||||||
if cache.lock.locked():
|
|
||||||
cache.lock.release()
|
|
||||||
|
|
||||||
if value is not None:
|
|
||||||
# if no more consumers, teardown the client
|
|
||||||
if cache.users <= 0:
|
|
||||||
log.warning(f'De-allocating resource for {key}')
|
|
||||||
|
|
||||||
# terminate mngr nursery
|
|
||||||
entry = cache.resources.get(ctx_key)
|
|
||||||
if entry:
|
|
||||||
_, no_more_users = entry
|
|
||||||
no_more_users.set()
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def open_cached_client(
|
async def open_cached_client(
|
||||||
brokername: str,
|
brokername: str,
|
||||||
|
@ -190,7 +72,7 @@ async def open_cached_client(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
brokermod = get_brokermod(brokername)
|
brokermod = get_brokermod(brokername)
|
||||||
async with maybe_open_ctx(
|
async with maybe_open_context(
|
||||||
key=brokername,
|
key=brokername,
|
||||||
mngr=brokermod.get_client(),
|
mngr=brokermod.get_client(),
|
||||||
) as (cache_hit, client):
|
) as (cache_hit, client):
|
||||||
|
|
|
@ -21,7 +21,7 @@ Profiling wrappers for internal libs.
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
_pg_profile: bool = False
|
_pg_profile: bool = True
|
||||||
|
|
||||||
|
|
||||||
def pg_profile_enabled() -> bool:
|
def pg_profile_enabled() -> bool:
|
||||||
|
|
|
@ -19,7 +19,7 @@ Binance backend
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import List, Dict, Any, Tuple, Union, Optional
|
from typing import List, Dict, Any, Tuple, Union, Optional, AsyncGenerator
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
@ -37,7 +37,7 @@ from .._cacheables import open_cached_client
|
||||||
from ._util import resproc, SymbolNotFound
|
from ._util import resproc, SymbolNotFound
|
||||||
from ..log import get_logger, get_console_log
|
from ..log import get_logger, get_console_log
|
||||||
from ..data import ShmArray
|
from ..data import ShmArray
|
||||||
from ..data._web_bs import open_autorecon_ws
|
from ..data._web_bs import open_autorecon_ws, NoBsWs
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -295,7 +295,7 @@ class AggTrade(BaseModel):
|
||||||
M: bool # Ignore
|
M: bool # Ignore
|
||||||
|
|
||||||
|
|
||||||
async def stream_messages(ws):
|
async def stream_messages(ws: NoBsWs) -> AsyncGenerator[NoBsWs, dict]:
|
||||||
|
|
||||||
timeouts = 0
|
timeouts = 0
|
||||||
while True:
|
while True:
|
||||||
|
@ -487,11 +487,20 @@ async def stream_quotes(
|
||||||
# signal to caller feed is ready for consumption
|
# signal to caller feed is ready for consumption
|
||||||
feed_is_live.set()
|
feed_is_live.set()
|
||||||
|
|
||||||
|
# import time
|
||||||
|
# last = time.time()
|
||||||
|
|
||||||
# start streaming
|
# start streaming
|
||||||
async for typ, msg in msg_gen:
|
async for typ, msg in msg_gen:
|
||||||
|
|
||||||
|
# period = time.time() - last
|
||||||
|
# hz = 1/period if period else float('inf')
|
||||||
|
# if hz > 60:
|
||||||
|
# log.info(f'Binance quotez : {hz}')
|
||||||
|
|
||||||
topic = msg['symbol'].lower()
|
topic = msg['symbol'].lower()
|
||||||
await send_chan.send({topic: msg})
|
await send_chan.send({topic: msg})
|
||||||
|
# last = time.time()
|
||||||
|
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
|
|
|
@ -1157,6 +1157,11 @@ async def backfill_bars(
|
||||||
https://github.com/pikers/piker/issues/128
|
https://github.com/pikers/piker/issues/128
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
log.warning(
|
||||||
|
'Decreasing history query count to 4 since, windows...')
|
||||||
|
count = 4
|
||||||
|
|
||||||
out, fails = await get_bars(sym)
|
out, fails = await get_bars(sym)
|
||||||
if out is None:
|
if out is None:
|
||||||
raise RuntimeError("Could not pull currrent history?!")
|
raise RuntimeError("Could not pull currrent history?!")
|
||||||
|
|
|
@ -43,11 +43,15 @@ def humanize(
|
||||||
if not number or number <= 0:
|
if not number or number <= 0:
|
||||||
return round(number, ndigits=digits)
|
return round(number, ndigits=digits)
|
||||||
|
|
||||||
mag = math.floor(math.log(number, 10))
|
mag = round(math.log(number, 10))
|
||||||
if mag < 3:
|
if mag < 3:
|
||||||
return round(number, ndigits=digits)
|
return round(number, ndigits=digits)
|
||||||
|
|
||||||
maxmag = max(itertools.takewhile(lambda key: mag >= key, _mag2suffix))
|
maxmag = max(
|
||||||
|
itertools.takewhile(
|
||||||
|
lambda key: mag >= key, _mag2suffix
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return "{value}{suffix}".format(
|
return "{value}{suffix}".format(
|
||||||
value=round(number/10**maxmag, ndigits=digits),
|
value=round(number/10**maxmag, ndigits=digits),
|
||||||
|
|
|
@ -20,6 +20,7 @@ In da suit parlances: "Execution management systems"
|
||||||
"""
|
"""
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from math import isnan
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
import time
|
import time
|
||||||
from typing import AsyncIterator, Callable
|
from typing import AsyncIterator, Callable
|
||||||
|
@ -47,9 +48,11 @@ log = get_logger(__name__)
|
||||||
|
|
||||||
# TODO: numba all of this
|
# TODO: numba all of this
|
||||||
def mk_check(
|
def mk_check(
|
||||||
|
|
||||||
trigger_price: float,
|
trigger_price: float,
|
||||||
known_last: float,
|
known_last: float,
|
||||||
action: str,
|
action: str,
|
||||||
|
|
||||||
) -> Callable[[float, float], bool]:
|
) -> Callable[[float, float], bool]:
|
||||||
"""Create a predicate for given ``exec_price`` based on last known
|
"""Create a predicate for given ``exec_price`` based on last known
|
||||||
price, ``known_last``.
|
price, ``known_last``.
|
||||||
|
@ -77,8 +80,7 @@ def mk_check(
|
||||||
|
|
||||||
return check_lt
|
return check_lt
|
||||||
|
|
||||||
else:
|
raise ValueError('trigger: {trigger_price}, last: {known_last}')
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -177,7 +179,15 @@ async def clear_dark_triggers(
|
||||||
tuple(execs.items())
|
tuple(execs.items())
|
||||||
):
|
):
|
||||||
|
|
||||||
if not pred or (ttype not in tf) or (not pred(price)):
|
if (
|
||||||
|
not pred or
|
||||||
|
ttype not in tf or
|
||||||
|
not pred(price)
|
||||||
|
):
|
||||||
|
log.debug(
|
||||||
|
f'skipping quote for {sym} '
|
||||||
|
f'{pred}, {ttype} not in {tf}?, {pred(price)}'
|
||||||
|
)
|
||||||
# majority of iterations will be non-matches
|
# majority of iterations will be non-matches
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -1005,7 +1015,8 @@ async def _emsd_main(
|
||||||
first_quote = feed.first_quotes[symbol]
|
first_quote = feed.first_quotes[symbol]
|
||||||
|
|
||||||
book = _router.get_dark_book(broker)
|
book = _router.get_dark_book(broker)
|
||||||
book.lasts[(broker, symbol)] = first_quote['last']
|
last = book.lasts[(broker, symbol)] = first_quote['last']
|
||||||
|
assert not isnan(last) # ib is a cucker but we've fixed it in the backend
|
||||||
|
|
||||||
# open a stream with the brokerd backend for order
|
# open a stream with the brokerd backend for order
|
||||||
# flow dialogue
|
# flow dialogue
|
||||||
|
@ -1035,7 +1046,7 @@ async def _emsd_main(
|
||||||
|
|
||||||
# signal to client that we're started and deliver
|
# signal to client that we're started and deliver
|
||||||
# all known pps and accounts for this ``brokerd``.
|
# all known pps and accounts for this ``brokerd``.
|
||||||
await ems_ctx.started((pp_msgs, relay.accounts))
|
await ems_ctx.started((pp_msgs, list(relay.accounts)))
|
||||||
|
|
||||||
# establish 2-way stream with requesting order-client and
|
# establish 2-way stream with requesting order-client and
|
||||||
# begin handling inbound order requests and updates
|
# begin handling inbound order requests and updates
|
||||||
|
|
|
@ -60,7 +60,7 @@ def repodir():
|
||||||
"""
|
"""
|
||||||
dirpath = os.path.abspath(
|
dirpath = os.path.abspath(
|
||||||
# we're 3 levels down in **this** module file
|
# we're 3 levels down in **this** module file
|
||||||
dirname(dirname(dirname(os.path.realpath(__file__))))
|
dirname(dirname(os.path.realpath(__file__)))
|
||||||
)
|
)
|
||||||
return dirpath
|
return dirpath
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ def load(
|
||||||
path = path or get_broker_conf_path()
|
path = path or get_broker_conf_path()
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
shutil.copyfile(
|
shutil.copyfile(
|
||||||
os.path.join(repodir(), 'data/brokers.toml'),
|
os.path.join(repodir(), 'config', 'brokers.toml'),
|
||||||
path,
|
path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -313,7 +313,8 @@ async def uniform_rate_send(
|
||||||
|
|
||||||
except trio.WouldBlock:
|
except trio.WouldBlock:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
rate = 1 / (now - last_send)
|
diff = now - last_send
|
||||||
|
rate = 1 / diff if diff else float('inf')
|
||||||
last_send = now
|
last_send = now
|
||||||
|
|
||||||
# log.info(f'{rate} Hz sending quotes') # \n{first_quote}')
|
# log.info(f'{rate} Hz sending quotes') # \n{first_quote}')
|
||||||
|
|
|
@ -272,9 +272,8 @@ class ShmArray:
|
||||||
return end
|
return end
|
||||||
|
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
# shoudl raise if diff detected
|
# should raise if diff detected
|
||||||
self.diff_err_fields(data)
|
self.diff_err_fields(data)
|
||||||
|
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
def diff_err_fields(
|
def diff_err_fields(
|
||||||
|
@ -395,6 +394,7 @@ def open_shm_array(
|
||||||
|
|
||||||
# "unlink" created shm on process teardown by
|
# "unlink" created shm on process teardown by
|
||||||
# pushing teardown calls onto actor context stack
|
# pushing teardown calls onto actor context stack
|
||||||
|
|
||||||
tractor._actor._lifetime_stack.callback(shmarr.close)
|
tractor._actor._lifetime_stack.callback(shmarr.close)
|
||||||
tractor._actor._lifetime_stack.callback(shmarr.destroy)
|
tractor._actor._lifetime_stack.callback(shmarr.destroy)
|
||||||
|
|
||||||
|
|
|
@ -133,9 +133,11 @@ def mk_symbol(
|
||||||
|
|
||||||
|
|
||||||
def from_df(
|
def from_df(
|
||||||
|
|
||||||
df: pd.DataFrame,
|
df: pd.DataFrame,
|
||||||
source=None,
|
source=None,
|
||||||
default_tf=None
|
default_tf=None
|
||||||
|
|
||||||
) -> np.recarray:
|
) -> np.recarray:
|
||||||
"""Convert OHLC formatted ``pandas.DataFrame`` to ``numpy.recarray``.
|
"""Convert OHLC formatted ``pandas.DataFrame`` to ``numpy.recarray``.
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ ToOlS fOr CoPInG wITh "tHE wEB" protocols.
|
||||||
"""
|
"""
|
||||||
from contextlib import asynccontextmanager, AsyncExitStack
|
from contextlib import asynccontextmanager, AsyncExitStack
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable, AsyncGenerator
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
@ -127,7 +127,7 @@ async def open_autorecon_ws(
|
||||||
|
|
||||||
# TODO: proper type annot smh
|
# TODO: proper type annot smh
|
||||||
fixture: Callable,
|
fixture: Callable,
|
||||||
):
|
) -> AsyncGenerator[tuple[...], NoBsWs]:
|
||||||
"""Apparently we can QoS for all sorts of reasons..so catch em.
|
"""Apparently we can QoS for all sorts of reasons..so catch em.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -34,11 +34,10 @@ import trio
|
||||||
from trio.abc import ReceiveChannel
|
from trio.abc import ReceiveChannel
|
||||||
from trio_typing import TaskStatus
|
from trio_typing import TaskStatus
|
||||||
import tractor
|
import tractor
|
||||||
# from tractor import _broadcast
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from ..brokers import get_brokermod
|
from ..brokers import get_brokermod
|
||||||
from .._cacheables import maybe_open_ctx
|
from .._cacheables import maybe_open_context
|
||||||
from ..log import get_logger, get_console_log
|
from ..log import get_logger, get_console_log
|
||||||
from .._daemon import (
|
from .._daemon import (
|
||||||
maybe_spawn_brokerd,
|
maybe_spawn_brokerd,
|
||||||
|
@ -247,7 +246,7 @@ async def allocate_persistent_feed(
|
||||||
|
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def attach_feed_bus(
|
async def open_feed_bus(
|
||||||
|
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
brokername: str,
|
brokername: str,
|
||||||
|
@ -364,7 +363,7 @@ async def open_sample_step_stream(
|
||||||
# XXX: this should be singleton on a host,
|
# XXX: this should be singleton on a host,
|
||||||
# a lone broker-daemon per provider should be
|
# a lone broker-daemon per provider should be
|
||||||
# created for all practical purposes
|
# created for all practical purposes
|
||||||
async with maybe_open_ctx(
|
async with maybe_open_context(
|
||||||
key=delay_s,
|
key=delay_s,
|
||||||
mngr=portal.open_stream_from(
|
mngr=portal.open_stream_from(
|
||||||
iter_ohlc_periods,
|
iter_ohlc_periods,
|
||||||
|
@ -507,7 +506,7 @@ async def open_feed(
|
||||||
|
|
||||||
portal.open_context(
|
portal.open_context(
|
||||||
|
|
||||||
attach_feed_bus,
|
open_feed_bus,
|
||||||
brokername=brokername,
|
brokername=brokername,
|
||||||
symbol=sym,
|
symbol=sym,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
|
@ -586,7 +585,7 @@ async def maybe_open_feed(
|
||||||
'''
|
'''
|
||||||
sym = symbols[0].lower()
|
sym = symbols[0].lower()
|
||||||
|
|
||||||
async with maybe_open_ctx(
|
async with maybe_open_context(
|
||||||
key=(brokername, sym),
|
key=(brokername, sym),
|
||||||
mngr=open_feed(
|
mngr=open_feed(
|
||||||
brokername,
|
brokername,
|
||||||
|
|
|
@ -34,7 +34,7 @@ from ..data import attach_shm_array
|
||||||
from ..data.feed import Feed
|
from ..data.feed import Feed
|
||||||
from ..data._sharedmem import ShmArray
|
from ..data._sharedmem import ShmArray
|
||||||
from ._momo import _rsi, _wma
|
from ._momo import _rsi, _wma
|
||||||
from ._volume import _tina_vwap
|
from ._volume import _tina_vwap, dolla_vlm
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ _fsp_builtins = {
|
||||||
'rsi': _rsi,
|
'rsi': _rsi,
|
||||||
'wma': _wma,
|
'wma': _wma,
|
||||||
'vwap': _tina_vwap,
|
'vwap': _tina_vwap,
|
||||||
|
'dolla_vlm': dolla_vlm,
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: things to figure the heck out:
|
# TODO: things to figure the heck out:
|
||||||
|
@ -90,7 +91,7 @@ async def fsp_compute(
|
||||||
func_name: str,
|
func_name: str,
|
||||||
func: Callable,
|
func: Callable,
|
||||||
|
|
||||||
attach_stream: bool = True,
|
attach_stream: bool = False,
|
||||||
task_status: TaskStatus[None] = trio.TASK_STATUS_IGNORED,
|
task_status: TaskStatus[None] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -144,10 +145,13 @@ async def fsp_compute(
|
||||||
profiler(f'{func_name} pushed history')
|
profiler(f'{func_name} pushed history')
|
||||||
profiler.finish()
|
profiler.finish()
|
||||||
|
|
||||||
|
# TODO: UGH, what is the right way to do something like this?
|
||||||
|
if not ctx._started_called:
|
||||||
|
await ctx.started(index)
|
||||||
|
|
||||||
# setup a respawn handle
|
# setup a respawn handle
|
||||||
with trio.CancelScope() as cs:
|
with trio.CancelScope() as cs:
|
||||||
tracker = TaskTracker(trio.Event(), cs)
|
tracker = TaskTracker(trio.Event(), cs)
|
||||||
await ctx.started(index)
|
|
||||||
task_status.started((tracker, index))
|
task_status.started((tracker, index))
|
||||||
profiler(f'{func_name} yield last index')
|
profiler(f'{func_name} yield last index')
|
||||||
|
|
||||||
|
|
|
@ -14,16 +14,20 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from typing import AsyncIterator, Optional
|
from typing import AsyncIterator, Optional, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from tractor.trionics._broadcast import AsyncReceiver
|
||||||
|
|
||||||
from ..data._normalize import iterticks
|
from ..data._normalize import iterticks
|
||||||
|
from ..data._sharedmem import ShmArray
|
||||||
|
|
||||||
|
|
||||||
def wap(
|
def wap(
|
||||||
|
|
||||||
signal: np.ndarray,
|
signal: np.ndarray,
|
||||||
weights: np.ndarray,
|
weights: np.ndarray,
|
||||||
|
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""Weighted average price from signal and weights.
|
"""Weighted average price from signal and weights.
|
||||||
|
|
||||||
|
@ -47,15 +51,22 @@ def wap(
|
||||||
|
|
||||||
|
|
||||||
async def _tina_vwap(
|
async def _tina_vwap(
|
||||||
source, #: AsyncStream[np.ndarray],
|
|
||||||
ohlcv: np.ndarray, # price time-frame "aware"
|
source: AsyncReceiver[dict],
|
||||||
|
ohlcv: ShmArray, # OHLC sampled history
|
||||||
|
|
||||||
|
# TODO: anchor logic (eg. to session start)
|
||||||
anchors: Optional[np.ndarray] = None,
|
anchors: Optional[np.ndarray] = None,
|
||||||
) -> AsyncIterator[np.ndarray]: # maybe something like like FspStream?
|
|
||||||
"""Streaming volume weighted moving average.
|
) -> Union[
|
||||||
|
AsyncIterator[np.ndarray],
|
||||||
|
float
|
||||||
|
]:
|
||||||
|
'''Streaming volume weighted moving average.
|
||||||
|
|
||||||
Calling this "tina" for now since we're using HLC3 instead of tick.
|
Calling this "tina" for now since we're using HLC3 instead of tick.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
if anchors is None:
|
if anchors is None:
|
||||||
# TODO:
|
# TODO:
|
||||||
# anchor to session start of data if possible
|
# anchor to session start of data if possible
|
||||||
|
@ -75,7 +86,6 @@ async def _tina_vwap(
|
||||||
# vwap_tot = h_vwap[-1]
|
# vwap_tot = h_vwap[-1]
|
||||||
|
|
||||||
async for quote in source:
|
async for quote in source:
|
||||||
|
|
||||||
for tick in iterticks(quote, types=['trade']):
|
for tick in iterticks(quote, types=['trade']):
|
||||||
|
|
||||||
# c, h, l, v = ohlcv.array[-1][
|
# c, h, l, v = ohlcv.array[-1][
|
||||||
|
@ -91,3 +101,44 @@ async def _tina_vwap(
|
||||||
|
|
||||||
# yield ((((o + h + l) / 3) * v) weights_tot) / v_tot
|
# yield ((((o + h + l) / 3) * v) weights_tot) / v_tot
|
||||||
yield w_tot / v_tot
|
yield w_tot / v_tot
|
||||||
|
|
||||||
|
|
||||||
|
async def dolla_vlm(
|
||||||
|
source: AsyncReceiver[dict],
|
||||||
|
ohlcv: ShmArray, # OHLC sampled history
|
||||||
|
|
||||||
|
) -> Union[
|
||||||
|
AsyncIterator[np.ndarray],
|
||||||
|
float
|
||||||
|
]:
|
||||||
|
a = ohlcv.array
|
||||||
|
chl3 = (a['close'] + a['high'] + a['low']) / 3
|
||||||
|
v = a['volume']
|
||||||
|
|
||||||
|
# history
|
||||||
|
yield chl3 * v
|
||||||
|
|
||||||
|
i = ohlcv.index
|
||||||
|
lvlm = 0
|
||||||
|
|
||||||
|
async for quote in source:
|
||||||
|
for tick in iterticks(quote):
|
||||||
|
|
||||||
|
# this computes tick-by-tick weightings from here forward
|
||||||
|
size = tick['size']
|
||||||
|
price = tick['price']
|
||||||
|
|
||||||
|
li = ohlcv.index
|
||||||
|
if li > i:
|
||||||
|
i = li
|
||||||
|
lvlm = 0
|
||||||
|
|
||||||
|
c, h, l, v = ohlcv.last()[
|
||||||
|
['close', 'high', 'low', 'volume']
|
||||||
|
][0]
|
||||||
|
|
||||||
|
lvlm += price * size
|
||||||
|
tina_lvlm = c+h+l/3 * v
|
||||||
|
# print(f' tinal vlm: {tina_lvlm}')
|
||||||
|
|
||||||
|
yield lvlm
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# piker: trading gear for hackers
|
||||||
|
# Copyright (C) Tyler Goodlet (in stewardship of piker0)
|
||||||
|
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
sugarz for trio/tractor conc peeps.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from typing import AsyncContextManager
|
||||||
|
from typing import TypeVar
|
||||||
|
from contextlib import asynccontextmanager as acm
|
||||||
|
|
||||||
|
import trio
|
||||||
|
|
||||||
|
|
||||||
|
# A regular invariant generic type
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
async def _enter_and_sleep(
|
||||||
|
|
||||||
|
mngr: AsyncContextManager[T],
|
||||||
|
to_yield: dict[int, T],
|
||||||
|
all_entered: trio.Event,
|
||||||
|
# task_status: TaskStatus[T] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
|
) -> T:
|
||||||
|
'''Open the async context manager deliver it's value
|
||||||
|
to this task's spawner and sleep until cancelled.
|
||||||
|
|
||||||
|
'''
|
||||||
|
async with mngr as value:
|
||||||
|
to_yield[id(mngr)] = value
|
||||||
|
|
||||||
|
if all(to_yield.values()):
|
||||||
|
all_entered.set()
|
||||||
|
|
||||||
|
# sleep until cancelled
|
||||||
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
||||||
|
@acm
|
||||||
|
async def async_enter_all(
|
||||||
|
|
||||||
|
*mngrs: list[AsyncContextManager[T]],
|
||||||
|
|
||||||
|
) -> tuple[T]:
|
||||||
|
|
||||||
|
to_yield = {}.fromkeys(id(mngr) for mngr in mngrs)
|
||||||
|
|
||||||
|
all_entered = trio.Event()
|
||||||
|
|
||||||
|
async with trio.open_nursery() as n:
|
||||||
|
for mngr in mngrs:
|
||||||
|
n.start_soon(
|
||||||
|
_enter_and_sleep,
|
||||||
|
mngr,
|
||||||
|
to_yield,
|
||||||
|
all_entered,
|
||||||
|
)
|
||||||
|
|
||||||
|
# deliver control once all managers have started up
|
||||||
|
await all_entered.wait()
|
||||||
|
yield tuple(to_yield.values())
|
||||||
|
|
||||||
|
# tear down all sleeper tasks thus triggering individual
|
||||||
|
# mngr ``__aexit__()``s.
|
||||||
|
n.cancel_scope.cancel()
|
|
@ -85,11 +85,11 @@ async def _async_main(
|
||||||
screen = godwidget.window.current_screen()
|
screen = godwidget.window.current_screen()
|
||||||
|
|
||||||
# configure graphics update throttling based on display refresh rate
|
# configure graphics update throttling based on display refresh rate
|
||||||
_display._clear_throttle_rate = min(
|
_display._quote_throttle_rate = min(
|
||||||
round(screen.refreshRate()),
|
round(screen.refreshRate()),
|
||||||
_display._clear_throttle_rate,
|
_display._quote_throttle_rate,
|
||||||
)
|
)
|
||||||
log.info(f'Set graphics update rate to {_display._clear_throttle_rate} Hz')
|
log.info(f'Set graphics update rate to {_display._quote_throttle_rate} Hz')
|
||||||
|
|
||||||
# TODO: do styling / themeing setup
|
# TODO: do styling / themeing setup
|
||||||
# _style.style_ze_sheets(godwidget)
|
# _style.style_ze_sheets(godwidget)
|
||||||
|
|
|
@ -25,6 +25,9 @@ from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QFrame,
|
QFrame,
|
||||||
QWidget,
|
QWidget,
|
||||||
|
QHBoxLayout,
|
||||||
|
QVBoxLayout,
|
||||||
|
QSplitter,
|
||||||
# QSizePolicy,
|
# QSizePolicy,
|
||||||
)
|
)
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -53,6 +56,7 @@ from ._style import (
|
||||||
)
|
)
|
||||||
from ..data.feed import Feed
|
from ..data.feed import Feed
|
||||||
from ..data._source import Symbol
|
from ..data._source import Symbol
|
||||||
|
from ..data._sharedmem import ShmArray
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._interaction import ChartView
|
from ._interaction import ChartView
|
||||||
from ._forms import FieldsForm
|
from ._forms import FieldsForm
|
||||||
|
@ -64,11 +68,11 @@ log = get_logger(__name__)
|
||||||
class GodWidget(QWidget):
|
class GodWidget(QWidget):
|
||||||
'''
|
'''
|
||||||
"Our lord and savior, the holy child of window-shua, there is no
|
"Our lord and savior, the holy child of window-shua, there is no
|
||||||
widget above thee." - 6|6
|
widget above thee." - 6||6
|
||||||
|
|
||||||
The highest level composed widget which contains layouts for
|
The highest level composed widget which contains layouts for
|
||||||
organizing lower level charts as well as other widgets used to
|
organizing charts as well as other sub-widgets used to control or
|
||||||
control or modify them.
|
modify them.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -80,19 +84,19 @@ class GodWidget(QWidget):
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.hbox = QtWidgets.QHBoxLayout(self)
|
self.hbox = QHBoxLayout(self)
|
||||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
self.hbox.setSpacing(6)
|
self.hbox.setSpacing(6)
|
||||||
self.hbox.setAlignment(Qt.AlignTop)
|
self.hbox.setAlignment(Qt.AlignTop)
|
||||||
|
|
||||||
self.vbox = QtWidgets.QVBoxLayout()
|
self.vbox = QVBoxLayout()
|
||||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||||
self.vbox.setSpacing(2)
|
self.vbox.setSpacing(2)
|
||||||
self.vbox.setAlignment(Qt.AlignTop)
|
self.vbox.setAlignment(Qt.AlignTop)
|
||||||
|
|
||||||
self.hbox.addLayout(self.vbox)
|
self.hbox.addLayout(self.vbox)
|
||||||
|
|
||||||
# self.toolbar_layout = QtWidgets.QHBoxLayout()
|
# self.toolbar_layout = QHBoxLayout()
|
||||||
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
# self.vbox.addLayout(self.toolbar_layout)
|
# self.vbox.addLayout(self.toolbar_layout)
|
||||||
|
|
||||||
|
@ -106,25 +110,8 @@ class GodWidget(QWidget):
|
||||||
# assigned in the startup func `_async_main()`
|
# assigned in the startup func `_async_main()`
|
||||||
self._root_n: trio.Nursery = None
|
self._root_n: trio.Nursery = None
|
||||||
|
|
||||||
def set_chart_symbol(
|
|
||||||
self,
|
|
||||||
symbol_key: str, # of form <fqsn>.<providername>
|
|
||||||
linkedsplits: 'LinkedSplits', # type: ignore
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
# re-sort org cache symbol list in LIFO order
|
|
||||||
cache = self._chart_cache
|
|
||||||
cache.pop(symbol_key, None)
|
|
||||||
cache[symbol_key] = linkedsplits
|
|
||||||
|
|
||||||
def get_chart_symbol(
|
|
||||||
self,
|
|
||||||
symbol_key: str,
|
|
||||||
) -> 'LinkedSplits': # type: ignore
|
|
||||||
return self._chart_cache.get(symbol_key)
|
|
||||||
|
|
||||||
# def init_timeframes_ui(self):
|
# def init_timeframes_ui(self):
|
||||||
# self.tf_layout = QtWidgets.QHBoxLayout()
|
# self.tf_layout = QHBoxLayout()
|
||||||
# self.tf_layout.setSpacing(0)
|
# self.tf_layout.setSpacing(0)
|
||||||
# self.tf_layout.setContentsMargins(0, 12, 0, 0)
|
# self.tf_layout.setContentsMargins(0, 12, 0, 0)
|
||||||
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
|
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
|
||||||
|
@ -145,6 +132,23 @@ class GodWidget(QWidget):
|
||||||
# self.strategy_box = StrategyBoxWidget(self)
|
# self.strategy_box = StrategyBoxWidget(self)
|
||||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||||
|
|
||||||
|
def set_chart_symbol(
|
||||||
|
self,
|
||||||
|
symbol_key: str, # of form <fqsn>.<providername>
|
||||||
|
linkedsplits: 'LinkedSplits', # type: ignore
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
# re-sort org cache symbol list in LIFO order
|
||||||
|
cache = self._chart_cache
|
||||||
|
cache.pop(symbol_key, None)
|
||||||
|
cache[symbol_key] = linkedsplits
|
||||||
|
|
||||||
|
def get_chart_symbol(
|
||||||
|
self,
|
||||||
|
symbol_key: str,
|
||||||
|
) -> 'LinkedSplits': # type: ignore
|
||||||
|
return self._chart_cache.get(symbol_key)
|
||||||
|
|
||||||
async def load_symbol(
|
async def load_symbol(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
|
@ -255,7 +259,7 @@ class ChartnPane(QFrame):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
sidepane: FieldsForm
|
sidepane: FieldsForm
|
||||||
hbox: QtWidgets.QHBoxLayout
|
hbox: QHBoxLayout
|
||||||
chart: Optional['ChartPlotWidget'] = None
|
chart: Optional['ChartPlotWidget'] = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -271,7 +275,7 @@ class ChartnPane(QFrame):
|
||||||
self.sidepane = sidepane
|
self.sidepane = sidepane
|
||||||
self.chart = None
|
self.chart = None
|
||||||
|
|
||||||
hbox = self.hbox = QtWidgets.QHBoxLayout(self)
|
hbox = self.hbox = QHBoxLayout(self)
|
||||||
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||||
hbox.setContentsMargins(0, 0, 0, 0)
|
hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
hbox.setSpacing(3)
|
hbox.setSpacing(3)
|
||||||
|
@ -281,21 +285,14 @@ class ChartnPane(QFrame):
|
||||||
|
|
||||||
class LinkedSplits(QWidget):
|
class LinkedSplits(QWidget):
|
||||||
'''
|
'''
|
||||||
Widget that holds a central chart plus derived
|
Composite that holds a central chart plus a set of (derived)
|
||||||
subcharts computed from the original data set apart
|
subcharts (usually computed from the original data) arranged in
|
||||||
by splitters for resizing.
|
a splitter for resizing.
|
||||||
|
|
||||||
A single internal references to the data is maintained
|
A single internal references to the data is maintained
|
||||||
for each chart and can be updated externally.
|
for each chart and can be updated externally.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
long_pen = pg.mkPen('#006000')
|
|
||||||
long_brush = pg.mkBrush('#00ff00')
|
|
||||||
short_pen = pg.mkPen('#600000')
|
|
||||||
short_brush = pg.mkBrush('#ff0000')
|
|
||||||
|
|
||||||
zoomIsDisabled = QtCore.pyqtSignal(bool)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
|
@ -325,11 +322,11 @@ class LinkedSplits(QWidget):
|
||||||
# self.xaxis_ind.setStyle(showValues=False)
|
# self.xaxis_ind.setStyle(showValues=False)
|
||||||
# self.xaxis.hide()
|
# self.xaxis.hide()
|
||||||
|
|
||||||
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
|
self.splitter = QSplitter(QtCore.Qt.Vertical)
|
||||||
self.splitter.setMidLineWidth(1)
|
self.splitter.setMidLineWidth(0)
|
||||||
self.splitter.setHandleWidth(0)
|
self.splitter.setHandleWidth(2)
|
||||||
|
|
||||||
self.layout = QtWidgets.QVBoxLayout(self)
|
self.layout = QVBoxLayout(self)
|
||||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.layout.addWidget(self.splitter)
|
self.layout.addWidget(self.splitter)
|
||||||
|
|
||||||
|
@ -341,20 +338,28 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
def set_split_sizes(
|
def set_split_sizes(
|
||||||
self,
|
self,
|
||||||
# prop: float = 0.375, # proportion allocated to consumer subcharts
|
prop: Optional[float] = None,
|
||||||
prop: float = 5/8,
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Set the proportion of space allocated for linked subcharts.
|
'''Set the proportion of space allocated for linked subcharts.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
ln = len(self.subplots)
|
||||||
|
|
||||||
|
if not prop:
|
||||||
|
# proportion allocated to consumer subcharts
|
||||||
|
if ln < 2:
|
||||||
|
prop = 1/(.666 * 6)
|
||||||
|
elif ln >= 2:
|
||||||
|
prop = 3/8
|
||||||
|
|
||||||
major = 1 - prop
|
major = 1 - prop
|
||||||
min_h_ind = int((self.height() * prop) / len(self.subplots))
|
min_h_ind = int((self.height() * prop) / ln)
|
||||||
|
|
||||||
sizes = [int(self.height() * major)]
|
sizes = [int(self.height() * major)]
|
||||||
sizes.extend([min_h_ind] * len(self.subplots))
|
sizes.extend([min_h_ind] * ln)
|
||||||
|
|
||||||
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
self.splitter.setSizes(sizes)
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
if self.chart is not None:
|
if self.chart is not None:
|
||||||
|
@ -374,16 +379,21 @@ class LinkedSplits(QWidget):
|
||||||
style: str = 'bar',
|
style: str = 'bar',
|
||||||
|
|
||||||
) -> 'ChartPlotWidget':
|
) -> 'ChartPlotWidget':
|
||||||
"""Start up and show main (price) chart and all linked subcharts.
|
'''Start up and show main (price) chart and all linked subcharts.
|
||||||
|
|
||||||
The data input struct array must include OHLC fields.
|
The data input struct array must include OHLC fields.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
# add crosshairs
|
# add crosshairs
|
||||||
self.cursor = Cursor(
|
self.cursor = Cursor(
|
||||||
linkedsplits=self,
|
linkedsplits=self,
|
||||||
digits=symbol.tick_size_digits,
|
digits=symbol.tick_size_digits,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# NOTE: atm the first (and only) OHLC price chart for the symbol
|
||||||
|
# is given a special reference but in the future there shouldn't
|
||||||
|
# be no distinction since we will have multiple symbols per
|
||||||
|
# view as part of "aggregate feeds".
|
||||||
self.chart = self.add_plot(
|
self.chart = self.add_plot(
|
||||||
|
|
||||||
name=symbol.key,
|
name=symbol.key,
|
||||||
|
@ -425,9 +435,7 @@ class LinkedSplits(QWidget):
|
||||||
**cpw_kwargs,
|
**cpw_kwargs,
|
||||||
|
|
||||||
) -> 'ChartPlotWidget':
|
) -> 'ChartPlotWidget':
|
||||||
'''Add (sub)plots to chart widget by name.
|
'''Add (sub)plots to chart widget by key.
|
||||||
|
|
||||||
If ``name`` == ``"main"`` the chart will be the the primary view.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if self.chart is None and not _is_main:
|
if self.chart is None and not _is_main:
|
||||||
|
@ -495,8 +503,9 @@ class LinkedSplits(QWidget):
|
||||||
cpw.plotItem.vb.linkedsplits = self
|
cpw.plotItem.vb.linkedsplits = self
|
||||||
cpw.setFrameStyle(
|
cpw.setFrameStyle(
|
||||||
QtWidgets.QFrame.StyledPanel
|
QtWidgets.QFrame.StyledPanel
|
||||||
# | QtWidgets.QFrame.Plain)
|
# | QtWidgets.QFrame.Plain
|
||||||
)
|
)
|
||||||
|
|
||||||
cpw.hideButtons()
|
cpw.hideButtons()
|
||||||
|
|
||||||
# XXX: gives us outline on backside of y-axis
|
# XXX: gives us outline on backside of y-axis
|
||||||
|
@ -515,7 +524,22 @@ class LinkedSplits(QWidget):
|
||||||
cpw.draw_ohlc(name, array, array_key=array_key)
|
cpw.draw_ohlc(name, array, array_key=array_key)
|
||||||
|
|
||||||
elif style == 'line':
|
elif style == 'line':
|
||||||
cpw.draw_curve(name, array, array_key=array_key)
|
cpw.draw_curve(
|
||||||
|
name,
|
||||||
|
array,
|
||||||
|
array_key=array_key,
|
||||||
|
color='default_light',
|
||||||
|
)
|
||||||
|
|
||||||
|
elif style == 'step':
|
||||||
|
cpw.draw_curve(
|
||||||
|
name,
|
||||||
|
array,
|
||||||
|
array_key=array_key,
|
||||||
|
step_mode=True,
|
||||||
|
color='davies',
|
||||||
|
fill_color='davies',
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Chart style {style} is currently unsupported")
|
raise ValueError(f"Chart style {style} is currently unsupported")
|
||||||
|
@ -523,14 +547,7 @@ class LinkedSplits(QWidget):
|
||||||
if not _is_main:
|
if not _is_main:
|
||||||
# track by name
|
# track by name
|
||||||
self.subplots[name] = cpw
|
self.subplots[name] = cpw
|
||||||
|
|
||||||
# if sidepane:
|
|
||||||
# # TODO: use a "panes" collection to manage this?
|
|
||||||
# qframe.setMaximumWidth(self.chart.sidepane.width())
|
|
||||||
# qframe.setMinimumWidth(self.chart.sidepane.width())
|
|
||||||
|
|
||||||
self.splitter.addWidget(qframe)
|
self.splitter.addWidget(qframe)
|
||||||
|
|
||||||
# scale split regions
|
# scale split regions
|
||||||
self.set_split_sizes()
|
self.set_split_sizes()
|
||||||
|
|
||||||
|
@ -586,6 +603,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
view_color: str = 'papas_special',
|
view_color: str = 'papas_special',
|
||||||
pen_color: str = 'bracket',
|
pen_color: str = 'bracket',
|
||||||
|
|
||||||
|
# TODO: load from config
|
||||||
|
use_open_gl: bool = False,
|
||||||
|
|
||||||
static_yrange: Optional[tuple[float, float]] = None,
|
static_yrange: Optional[tuple[float, float]] = None,
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
@ -600,9 +620,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# parent=None,
|
# parent=None,
|
||||||
# plotItem=None,
|
# plotItem=None,
|
||||||
# antialias=True,
|
# antialias=True,
|
||||||
useOpenGL=True,
|
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
self.useOpenGL(use_open_gl)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.data_key = data_key
|
self.data_key = data_key
|
||||||
self.linked = linkedsplits
|
self.linked = linkedsplits
|
||||||
|
@ -619,7 +639,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
'ohlc': array,
|
'ohlc': array,
|
||||||
}
|
}
|
||||||
self._graphics = {} # registry of underlying graphics
|
self._graphics = {} # registry of underlying graphics
|
||||||
self._overlays = set() # registry of overlay curve names
|
# registry of overlay curve names
|
||||||
|
self._overlays: dict[str, ShmArray] = {}
|
||||||
|
|
||||||
self._feeds: dict[Symbol, Feed] = {}
|
self._feeds: dict[Symbol, Feed] = {}
|
||||||
|
|
||||||
|
@ -732,6 +753,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self._vb.setXRange(
|
self._vb.setXRange(
|
||||||
min=l + 1,
|
min=l + 1,
|
||||||
max=r + 1,
|
max=r + 1,
|
||||||
|
|
||||||
# TODO: holy shit, wtf dude... why tf would this not be 0 by
|
# TODO: holy shit, wtf dude... why tf would this not be 0 by
|
||||||
# default... speechless.
|
# default... speechless.
|
||||||
padding=0,
|
padding=0,
|
||||||
|
@ -772,7 +794,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
update_func=ContentsLabel.update_from_ohlc,
|
update_func=ContentsLabel.update_from_ohlc,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._add_sticky(name)
|
self._add_sticky(name, bg_color='davies')
|
||||||
|
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
|
@ -784,7 +806,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
array_key: Optional[str] = None,
|
array_key: Optional[str] = None,
|
||||||
overlay: bool = False,
|
overlay: bool = False,
|
||||||
color: str = 'default_light',
|
color: Optional[str] = None,
|
||||||
add_label: bool = True,
|
add_label: bool = True,
|
||||||
|
|
||||||
**pdi_kwargs,
|
**pdi_kwargs,
|
||||||
|
@ -794,15 +816,18 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
the input array ``data``.
|
the input array ``data``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
_pdi_defaults = {
|
color = color or self.pen_color or 'default_light'
|
||||||
'pen': pg.mkPen(hcolor(color)),
|
pdi_kwargs.update({
|
||||||
}
|
'color': color
|
||||||
pdi_kwargs.update(_pdi_defaults)
|
})
|
||||||
|
|
||||||
data_key = array_key or name
|
data_key = array_key or name
|
||||||
|
|
||||||
|
# pg internals for reference.
|
||||||
# curve = pg.PlotDataItem(
|
# curve = pg.PlotDataItem(
|
||||||
# curve = pg.PlotCurveItem(
|
# curve = pg.PlotCurveItem(
|
||||||
|
|
||||||
|
# yah, we wrote our own B)
|
||||||
curve = FastAppendCurve(
|
curve = FastAppendCurve(
|
||||||
y=data[data_key],
|
y=data[data_key],
|
||||||
x=data['index'],
|
x=data['index'],
|
||||||
|
@ -840,14 +865,14 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
if overlay:
|
if overlay:
|
||||||
anchor_at = ('bottom', 'left')
|
anchor_at = ('bottom', 'left')
|
||||||
self._overlays.add(name)
|
self._overlays[name] = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
anchor_at = ('top', 'left')
|
anchor_at = ('top', 'left')
|
||||||
|
|
||||||
# TODO: something instead of stickies for overlays
|
# TODO: something instead of stickies for overlays
|
||||||
# (we need something that avoids clutter on x-axis).
|
# (we need something that avoids clutter on x-axis).
|
||||||
self._add_sticky(name, bg_color='default_light')
|
self._add_sticky(name, bg_color=color)
|
||||||
|
|
||||||
if self.linked.cursor:
|
if self.linked.cursor:
|
||||||
self.linked.cursor.add_curve_cursor(self, curve)
|
self.linked.cursor.add_curve_cursor(self, curve)
|
||||||
|
@ -861,6 +886,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
return curve
|
return curve
|
||||||
|
|
||||||
|
# TODO: make this a ctx mngr
|
||||||
def _add_sticky(
|
def _add_sticky(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
|
@ -890,46 +916,50 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
def update_ohlc_from_array(
|
def update_ohlc_from_array(
|
||||||
self,
|
self,
|
||||||
name: str,
|
|
||||||
|
graphics_name: str,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> pg.GraphicsObject:
|
|
||||||
"""Update the named internal graphics from ``array``.
|
|
||||||
|
|
||||||
"""
|
) -> pg.GraphicsObject:
|
||||||
|
'''Update the named internal graphics from ``array``.
|
||||||
|
|
||||||
|
'''
|
||||||
self._arrays['ohlc'] = array
|
self._arrays['ohlc'] = array
|
||||||
graphics = self._graphics[name]
|
graphics = self._graphics[graphics_name]
|
||||||
graphics.update_from_array(array, **kwargs)
|
graphics.update_from_array(array, **kwargs)
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
def update_curve_from_array(
|
def update_curve_from_array(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
name: str,
|
graphics_name: str,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
array_key: Optional[str] = None,
|
array_key: Optional[str] = None,
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> pg.GraphicsObject:
|
) -> pg.GraphicsObject:
|
||||||
"""Update the named internal graphics from ``array``.
|
'''Update the named internal graphics from ``array``.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
|
assert len(array)
|
||||||
|
data_key = array_key or graphics_name
|
||||||
|
|
||||||
data_key = array_key or name
|
if graphics_name not in self._overlays:
|
||||||
if name not in self._overlays:
|
|
||||||
self._arrays['ohlc'] = array
|
self._arrays['ohlc'] = array
|
||||||
else:
|
else:
|
||||||
self._arrays[data_key] = array
|
self._arrays[data_key] = array
|
||||||
|
|
||||||
curve = self._graphics[name]
|
curve = self._graphics[graphics_name]
|
||||||
|
|
||||||
if len(array):
|
# NOTE: back when we weren't implementing the curve graphics
|
||||||
# TODO: we should instead implement a diff based
|
# ourselves you'd have updates using this method:
|
||||||
# "only update with new items" on the pg.PlotCurveItem
|
# curve.setData(y=array[graphics_name], x=array['index'], **kwargs)
|
||||||
# one place to dig around this might be the `QBackingStore`
|
|
||||||
# https://doc.qt.io/qt-5/qbackingstore.html
|
# NOTE: graphics **must** implement a diff based update
|
||||||
# curve.setData(y=array[name], x=array['index'], **kwargs)
|
# operation where an internal ``FastUpdateCurve._xrange`` is
|
||||||
|
# used to determine if the underlying path needs to be
|
||||||
|
# pre/ap-pended.
|
||||||
curve.update_from_array(
|
curve.update_from_array(
|
||||||
x=array['index'],
|
x=array['index'],
|
||||||
y=array[data_key],
|
y=array[data_key],
|
||||||
|
@ -941,16 +971,23 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
def _set_yrange(
|
def _set_yrange(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|
||||||
yrange: Optional[tuple[float, float]] = None,
|
yrange: Optional[tuple[float, float]] = None,
|
||||||
range_margin: float = 0.06,
|
range_margin: float = 0.06,
|
||||||
|
bars_range: Optional[tuple[int, int, int, int]] = None,
|
||||||
|
|
||||||
|
# flag to prevent triggering sibling charts from the same linked
|
||||||
|
# set from recursion errors.
|
||||||
|
autoscale_linked_plots: bool = True,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the viewable y-range based on embedded data.
|
'''Set the viewable y-range based on embedded data.
|
||||||
|
|
||||||
This adds auto-scaling like zoom on the scroll wheel such
|
This adds auto-scaling like zoom on the scroll wheel such
|
||||||
that data always fits nicely inside the current view of the
|
that data always fits nicely inside the current view of the
|
||||||
data set.
|
data set.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
set_range = True
|
set_range = True
|
||||||
|
|
||||||
if self._static_yrange == 'axis':
|
if self._static_yrange == 'axis':
|
||||||
|
@ -966,52 +1003,50 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# Determine max, min y values in viewable x-range from data.
|
# Determine max, min y values in viewable x-range from data.
|
||||||
# Make sure min bars/datums on screen is adhered.
|
# Make sure min bars/datums on screen is adhered.
|
||||||
|
|
||||||
l, lbar, rbar, r = self.bars_range()
|
l, lbar, rbar, r = bars_range or self.bars_range()
|
||||||
|
|
||||||
# figure out x-range in view such that user can scroll "off"
|
if autoscale_linked_plots:
|
||||||
# the data set up to the point where ``_min_points_to_show``
|
# avoid recursion by sibling plots
|
||||||
# are left.
|
linked = self.linked
|
||||||
# view_len = r - l
|
plots = list(linked.subplots.copy().values())
|
||||||
|
main = linked.chart
|
||||||
|
if main:
|
||||||
|
plots.append(main)
|
||||||
|
|
||||||
|
for chart in plots:
|
||||||
|
if chart and not chart._static_yrange:
|
||||||
|
chart._set_yrange(
|
||||||
|
bars_range=(l, lbar, rbar, r),
|
||||||
|
autoscale_linked_plots=False,
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: logic to check if end of bars in view
|
# TODO: logic to check if end of bars in view
|
||||||
# extra = view_len - _min_points_to_show
|
# extra = view_len - _min_points_to_show
|
||||||
|
|
||||||
# begin = self._arrays['ohlc'][0]['index'] - extra
|
# begin = self._arrays['ohlc'][0]['index'] - extra
|
||||||
|
|
||||||
# # end = len(self._arrays['ohlc']) - 1 + extra
|
# # end = len(self._arrays['ohlc']) - 1 + extra
|
||||||
# end = self._arrays['ohlc'][-1]['index'] - 1 + extra
|
# end = self._arrays['ohlc'][-1]['index'] - 1 + extra
|
||||||
|
|
||||||
# XXX: test code for only rendering lines for the bars in view.
|
|
||||||
# This turns out to be very very poor perf when scaling out to
|
|
||||||
# many bars (think > 1k) on screen.
|
|
||||||
# name = self.name
|
|
||||||
# bars = self._graphics[self.name]
|
|
||||||
# bars.draw_lines(
|
|
||||||
# istart=max(lbar, l), iend=min(rbar, r), just_history=True)
|
|
||||||
|
|
||||||
# bars_len = rbar - lbar
|
# bars_len = rbar - lbar
|
||||||
# log.debug(
|
# log.debug(
|
||||||
# f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n"
|
# f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n"
|
||||||
# f"view_len: {view_len}, bars_len: {bars_len}\n"
|
# f"view_len: {view_len}, bars_len: {bars_len}\n"
|
||||||
# f"begin: {begin}, end: {end}, extra: {extra}"
|
# f"begin: {begin}, end: {end}, extra: {extra}"
|
||||||
# )
|
# )
|
||||||
# self._set_xlimits(begin, end)
|
|
||||||
|
|
||||||
# TODO: this should be some kind of numpy view api
|
|
||||||
# bars = self._arrays['ohlc'][lbar:rbar]
|
|
||||||
|
|
||||||
a = self._arrays['ohlc']
|
a = self._arrays['ohlc']
|
||||||
ifirst = a[0]['index']
|
ifirst = a[0]['index']
|
||||||
bars = a[lbar - ifirst:rbar - ifirst + 1]
|
bars = a[lbar - ifirst:rbar - ifirst + 1]
|
||||||
|
|
||||||
if not len(bars):
|
if not len(bars):
|
||||||
# likely no data loaded yet or extreme scrolling?
|
# likely no data loaded yet or extreme scrolling?
|
||||||
log.error(f"WTF bars_range = {lbar}:{rbar}")
|
log.error(f"WTF bars_range = {lbar}:{rbar}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.data_key != self.linked.symbol.key:
|
if self.data_key != self.linked.symbol.key:
|
||||||
bars = a[self.data_key]
|
bars = bars[self.data_key]
|
||||||
ylow = np.nanmin(bars)
|
ylow = np.nanmin(bars)
|
||||||
yhigh = np.nanmax((bars))
|
yhigh = np.nanmax(bars)
|
||||||
|
# print(f'{(ylow, yhigh)}')
|
||||||
else:
|
else:
|
||||||
# just the std ohlc bars
|
# just the std ohlc bars
|
||||||
ylow = np.nanmin(bars['low'])
|
ylow = np.nanmin(bars['low'])
|
||||||
|
@ -1072,7 +1107,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# TODO: this should go onto some sort of
|
# TODO: this should go onto some sort of
|
||||||
# data-view strimg thinger..right?
|
# data-view strimg thinger..right?
|
||||||
ohlc = self._shm.array
|
ohlc = self._shm.array
|
||||||
# ohlc = chart._shm.array
|
|
||||||
|
|
||||||
# XXX: not sure why the time is so off here
|
# XXX: not sure why the time is so off here
|
||||||
# looks like we're gonna have to do some fixing..
|
# looks like we're gonna have to do some fixing..
|
||||||
|
|
|
@ -18,25 +18,105 @@
|
||||||
Fast, smooth, sexy curves.
|
Fast, smooth, sexy curves.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from typing import Tuple
|
from typing import Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtGui, QtWidgets
|
||||||
|
from PyQt5.QtCore import (
|
||||||
|
QLineF,
|
||||||
|
QSizeF,
|
||||||
|
QRectF,
|
||||||
|
QPointF,
|
||||||
|
)
|
||||||
|
|
||||||
from .._profile import pg_profile_enabled
|
from .._profile import pg_profile_enabled
|
||||||
|
from ._style import hcolor
|
||||||
|
|
||||||
|
|
||||||
|
def step_path_arrays_from_1d(
|
||||||
|
x: np.ndarray,
|
||||||
|
y: np.ndarray,
|
||||||
|
include_endpoints: bool = False,
|
||||||
|
|
||||||
|
) -> (np.ndarray, np.ndarray):
|
||||||
|
'''Generate a "step mode" curve aligned with OHLC style bars
|
||||||
|
such that each segment spans each bar (aka "centered" style).
|
||||||
|
|
||||||
|
'''
|
||||||
|
y_out = y.copy()
|
||||||
|
x_out = x.copy()
|
||||||
|
x2 = np.empty(
|
||||||
|
# the data + 2 endpoints on either end for
|
||||||
|
# "termination of the path".
|
||||||
|
(len(x) + 1, 2),
|
||||||
|
# we want to align with OHLC or other sampling style
|
||||||
|
# bars likely so we need fractinal values
|
||||||
|
dtype=float,
|
||||||
|
)
|
||||||
|
x2[0] = x[0] - 0.5
|
||||||
|
x2[1] = x[0] + 0.5
|
||||||
|
x2[1:] = x[:, np.newaxis] + 0.5
|
||||||
|
|
||||||
|
# flatten to 1-d
|
||||||
|
x_out = x2.reshape(x2.size)
|
||||||
|
|
||||||
|
# we create a 1d with 2 extra indexes to
|
||||||
|
# hold the start and (current) end value for the steps
|
||||||
|
# on either end
|
||||||
|
y2 = np.empty((len(y), 2), dtype=y.dtype)
|
||||||
|
y2[:] = y[:, np.newaxis]
|
||||||
|
|
||||||
|
y_out = np.empty(
|
||||||
|
2*len(y) + 2,
|
||||||
|
dtype=y.dtype
|
||||||
|
)
|
||||||
|
|
||||||
|
# flatten and set 0 endpoints
|
||||||
|
y_out[1:-1] = y2.reshape(y2.size)
|
||||||
|
y_out[0] = 0
|
||||||
|
y_out[-1] = 0
|
||||||
|
|
||||||
|
if not include_endpoints:
|
||||||
|
return x_out[:-1], y_out[:-1]
|
||||||
|
|
||||||
|
else:
|
||||||
|
return x_out, y_out
|
||||||
|
|
||||||
|
|
||||||
# TODO: got a feeling that dropping this inheritance gets us even more speedups
|
# TODO: got a feeling that dropping this inheritance gets us even more speedups
|
||||||
class FastAppendCurve(pg.PlotCurveItem):
|
class FastAppendCurve(pg.PlotCurveItem):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
step_mode: bool = False,
|
||||||
|
color: str = 'default_lightest',
|
||||||
|
fill_color: Optional[str] = None,
|
||||||
|
|
||||||
|
**kwargs
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
# TODO: we can probably just dispense with the parent since
|
# TODO: we can probably just dispense with the parent since
|
||||||
# we're basically only using the pen setting now...
|
# we're basically only using the pen setting now...
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self._last_line: QtCore.QLineF = None
|
self._xrange: tuple[int, int] = self.dataBounds(ax=0)
|
||||||
self._xrange: Tuple[int, int] = self.dataBounds(ax=0)
|
|
||||||
|
# all history of curve is drawn in single px thickness
|
||||||
|
self.setPen(hcolor(color))
|
||||||
|
|
||||||
|
# last segment is drawn in 2px thickness for emphasis
|
||||||
|
self.last_step_pen = pg.mkPen(hcolor(color), width=2)
|
||||||
|
self._last_line: QLineF = None
|
||||||
|
self._last_step_rect: QRectF = None
|
||||||
|
|
||||||
|
# flat-top style histogram-like discrete curve
|
||||||
|
self._step_mode: bool = step_mode
|
||||||
|
|
||||||
|
self._fill = False
|
||||||
|
self.setBrush(hcolor(fill_color or color))
|
||||||
|
|
||||||
# TODO: one question still remaining is if this makes trasform
|
# TODO: one question still remaining is if this makes trasform
|
||||||
# interactions slower (such as zooming) and if so maybe if/when
|
# interactions slower (such as zooming) and if so maybe if/when
|
||||||
|
@ -46,8 +126,9 @@ class FastAppendCurve(pg.PlotCurveItem):
|
||||||
|
|
||||||
def update_from_array(
|
def update_from_array(
|
||||||
self,
|
self,
|
||||||
x,
|
x: np.ndarray,
|
||||||
y,
|
y: np.ndarray,
|
||||||
|
|
||||||
) -> QtGui.QPainterPath:
|
) -> QtGui.QPainterPath:
|
||||||
|
|
||||||
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
|
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
|
||||||
|
@ -56,17 +137,33 @@ class FastAppendCurve(pg.PlotCurveItem):
|
||||||
# print(f"xrange: {self._xrange}")
|
# print(f"xrange: {self._xrange}")
|
||||||
istart, istop = self._xrange
|
istart, istop = self._xrange
|
||||||
|
|
||||||
|
# compute the length diffs between the first/last index entry in
|
||||||
|
# the input data and the last indexes we have on record from the
|
||||||
|
# last time we updated the curve index.
|
||||||
prepend_length = istart - x[0]
|
prepend_length = istart - x[0]
|
||||||
append_length = x[-1] - istop
|
append_length = x[-1] - istop
|
||||||
|
|
||||||
if self.path is None or prepend_length:
|
# step mode: draw flat top discrete "step"
|
||||||
|
# over the index space for each datum.
|
||||||
|
if self._step_mode:
|
||||||
|
x_out, y_out = step_path_arrays_from_1d(x[:-1], y[:-1])
|
||||||
|
|
||||||
|
else:
|
||||||
|
# by default we only pull data up to the last (current) index
|
||||||
|
x_out, y_out = x[:-1], y[:-1]
|
||||||
|
|
||||||
|
if self.path is None or prepend_length > 0:
|
||||||
self.path = pg.functions.arrayToQPath(
|
self.path = pg.functions.arrayToQPath(
|
||||||
x[:-1],
|
x_out,
|
||||||
y[:-1],
|
y_out,
|
||||||
connect='all'
|
connect='all',
|
||||||
|
finiteCheck=False,
|
||||||
)
|
)
|
||||||
profiler('generate fresh path')
|
profiler('generate fresh path')
|
||||||
|
|
||||||
|
# if self._step_mode:
|
||||||
|
# self.path.closeSubpath()
|
||||||
|
|
||||||
# TODO: get this working - right now it's giving heck on vwap...
|
# TODO: get this working - right now it's giving heck on vwap...
|
||||||
# if prepend_length:
|
# if prepend_length:
|
||||||
# breakpoint()
|
# breakpoint()
|
||||||
|
@ -83,7 +180,16 @@ class FastAppendCurve(pg.PlotCurveItem):
|
||||||
# # self.path.moveTo(new_x[0], new_y[0])
|
# # self.path.moveTo(new_x[0], new_y[0])
|
||||||
# self.path.connectPath(old_path)
|
# self.path.connectPath(old_path)
|
||||||
|
|
||||||
if append_length:
|
elif append_length > 0:
|
||||||
|
if self._step_mode:
|
||||||
|
new_x, new_y = step_path_arrays_from_1d(
|
||||||
|
x[-append_length - 2:-1],
|
||||||
|
y[-append_length - 2:-1],
|
||||||
|
)
|
||||||
|
new_x = new_x[1:]
|
||||||
|
new_y = new_y[1:]
|
||||||
|
|
||||||
|
else:
|
||||||
# print(f"append_length: {append_length}")
|
# print(f"append_length: {append_length}")
|
||||||
new_x = x[-append_length - 2:-1]
|
new_x = x[-append_length - 2:-1]
|
||||||
new_y = y[-append_length - 2:-1]
|
new_y = y[-append_length - 2:-1]
|
||||||
|
@ -92,12 +198,29 @@ class FastAppendCurve(pg.PlotCurveItem):
|
||||||
append_path = pg.functions.arrayToQPath(
|
append_path = pg.functions.arrayToQPath(
|
||||||
new_x,
|
new_x,
|
||||||
new_y,
|
new_y,
|
||||||
connect='all'
|
connect='all',
|
||||||
|
# finiteCheck=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
path = self.path
|
||||||
|
|
||||||
|
# other merging ideas:
|
||||||
|
# https://stackoverflow.com/questions/8936225/how-to-merge-qpainterpaths
|
||||||
|
if self._step_mode:
|
||||||
|
if self._fill:
|
||||||
|
# XXX: super slow set "union" op
|
||||||
|
self.path = self.path.united(append_path).simplified()
|
||||||
|
|
||||||
|
# path.addPath(append_path)
|
||||||
|
# path.closeSubpath()
|
||||||
|
else:
|
||||||
|
# path.addPath(append_path)
|
||||||
|
self.path.connectPath(append_path)
|
||||||
|
else:
|
||||||
# print(f"append_path br: {append_path.boundingRect()}")
|
# print(f"append_path br: {append_path.boundingRect()}")
|
||||||
# self.path.moveTo(new_x[0], new_y[0])
|
# self.path.moveTo(new_x[0], new_y[0])
|
||||||
# self.path.connectPath(append_path)
|
# self.path.connectPath(append_path)
|
||||||
self.path.connectPath(append_path)
|
path.connectPath(append_path)
|
||||||
|
|
||||||
# XXX: pretty annoying but, without this there's little
|
# XXX: pretty annoying but, without this there's little
|
||||||
# artefacts on the append updates to the curve...
|
# artefacts on the append updates to the curve...
|
||||||
|
@ -112,8 +235,23 @@ class FastAppendCurve(pg.PlotCurveItem):
|
||||||
self.xData = x
|
self.xData = x
|
||||||
self.yData = y
|
self.yData = y
|
||||||
|
|
||||||
self._xrange = x[0], x[-1]
|
x0, x_last = self._xrange = x[0], x[-1]
|
||||||
self._last_line = QtCore.QLineF(x[-2], y[-2], x[-1], y[-1])
|
y_last = y[-1]
|
||||||
|
|
||||||
|
if self._step_mode:
|
||||||
|
self._last_line = QLineF(
|
||||||
|
x_last - 0.5, 0,
|
||||||
|
x_last + 0.5, 0,
|
||||||
|
)
|
||||||
|
self._last_step_rect = QRectF(
|
||||||
|
x_last - 0.5, 0,
|
||||||
|
x_last + 0.5, y_last
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._last_line = QLineF(
|
||||||
|
x[-2], y[-2],
|
||||||
|
x[-1], y_last
|
||||||
|
)
|
||||||
|
|
||||||
# trigger redraw of path
|
# trigger redraw of path
|
||||||
# do update before reverting to cache mode
|
# do update before reverting to cache mode
|
||||||
|
@ -143,13 +281,13 @@ class FastAppendCurve(pg.PlotCurveItem):
|
||||||
w = hb_size.width() + 1
|
w = hb_size.width() + 1
|
||||||
h = hb_size.height() + 1
|
h = hb_size.height() + 1
|
||||||
|
|
||||||
br = QtCore.QRectF(
|
br = QRectF(
|
||||||
|
|
||||||
# top left
|
# top left
|
||||||
QtCore.QPointF(hb.topLeft()),
|
QPointF(hb.topLeft()),
|
||||||
|
|
||||||
# total size
|
# total size
|
||||||
QtCore.QSizeF(w, h)
|
QSizeF(w, h)
|
||||||
)
|
)
|
||||||
# print(f'bounding rect: {br}')
|
# print(f'bounding rect: {br}')
|
||||||
return br
|
return br
|
||||||
|
@ -164,9 +302,26 @@ class FastAppendCurve(pg.PlotCurveItem):
|
||||||
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
|
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
|
||||||
# p.setRenderHint(p.Antialiasing, True)
|
# p.setRenderHint(p.Antialiasing, True)
|
||||||
|
|
||||||
p.setPen(self.opts['pen'])
|
if self._step_mode:
|
||||||
|
|
||||||
|
brush = self.opts['brush']
|
||||||
|
# p.drawLines(*tuple(filter(bool, self._last_step_lines)))
|
||||||
|
# p.drawRect(self._last_step_rect)
|
||||||
|
p.fillRect(self._last_step_rect, brush)
|
||||||
|
|
||||||
|
# p.drawPath(self.path)
|
||||||
|
|
||||||
|
# profiler('.drawPath()')
|
||||||
|
|
||||||
|
# else:
|
||||||
|
p.setPen(self.last_step_pen)
|
||||||
p.drawLine(self._last_line)
|
p.drawLine(self._last_line)
|
||||||
profiler('.drawLine()')
|
profiler('.drawLine()')
|
||||||
|
|
||||||
|
p.setPen(self.opts['pen'])
|
||||||
p.drawPath(self.path)
|
p.drawPath(self.path)
|
||||||
profiler('.drawPath()')
|
profiler('.drawPath()')
|
||||||
|
|
||||||
|
if self._fill:
|
||||||
|
print('FILLED')
|
||||||
|
p.fillPath(self.path, brush)
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -61,7 +61,9 @@ _do_overrides()
|
||||||
# XXX: pretty sure none of this shit works on linux as per:
|
# XXX: pretty sure none of this shit works on linux as per:
|
||||||
# https://bugreports.qt.io/browse/QTBUG-53022
|
# https://bugreports.qt.io/browse/QTBUG-53022
|
||||||
# it seems to work on windows.. no idea wtf is up.
|
# it seems to work on windows.. no idea wtf is up.
|
||||||
|
is_windows = False
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
|
is_windows = True
|
||||||
|
|
||||||
# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute
|
# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute
|
||||||
# must be set before creating the application
|
# must be set before creating the application
|
||||||
|
@ -182,6 +184,8 @@ def run_qtractor(
|
||||||
|
|
||||||
window.main_widget = main_widget
|
window.main_widget = main_widget
|
||||||
window.setCentralWidget(instance)
|
window.setCentralWidget(instance)
|
||||||
|
if is_windows:
|
||||||
|
window.configure_to_desktop()
|
||||||
|
|
||||||
# actually render to screen
|
# actually render to screen
|
||||||
window.show()
|
window.show()
|
||||||
|
|
|
@ -732,7 +732,7 @@ def mk_order_pane_layout(
|
||||||
|
|
||||||
) -> FieldsForm:
|
) -> FieldsForm:
|
||||||
|
|
||||||
font_size: int = _font.px_size - 1
|
font_size: int = _font.px_size - 2
|
||||||
|
|
||||||
# TODO: maybe just allocate the whole fields form here
|
# TODO: maybe just allocate the whole fields form here
|
||||||
# and expect an async ctx entry?
|
# and expect an async ctx entry?
|
||||||
|
|
|
@ -341,7 +341,14 @@ class ChartView(ViewBox):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
):
|
):
|
||||||
super().__init__(parent=parent, **kwargs)
|
super().__init__(
|
||||||
|
parent=parent,
|
||||||
|
# TODO: look into the default view padding
|
||||||
|
# support that might replace somem of our
|
||||||
|
# ``ChartPlotWidget._set_yrange()`
|
||||||
|
# defaultPadding=0.,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# disable vertical scrolling
|
# disable vertical scrolling
|
||||||
self.setMouseEnabled(x=True, y=False)
|
self.setMouseEnabled(x=True, y=False)
|
||||||
|
@ -533,7 +540,6 @@ class ChartView(ViewBox):
|
||||||
# self.updateScaleBox(ev.buttonDownPos(), ev.pos())
|
# self.updateScaleBox(ev.buttonDownPos(), ev.pos())
|
||||||
else:
|
else:
|
||||||
# default bevavior: click to pan view
|
# default bevavior: click to pan view
|
||||||
|
|
||||||
tr = self.childGroup.transform()
|
tr = self.childGroup.transform()
|
||||||
tr = fn.invertQTransform(tr)
|
tr = fn.invertQTransform(tr)
|
||||||
tr = tr.map(dif*mask) - tr.map(Point(0, 0))
|
tr = tr.map(dif*mask) - tr.map(Point(0, 0))
|
||||||
|
|
|
@ -146,7 +146,7 @@ def path_arrays_from_ohlc(
|
||||||
# specifies that the first edge is never connected to the
|
# specifies that the first edge is never connected to the
|
||||||
# prior bars last edge thus providing a small "gap"/"space"
|
# prior bars last edge thus providing a small "gap"/"space"
|
||||||
# between bars determined by ``bar_gap``.
|
# between bars determined by ``bar_gap``.
|
||||||
c[istart:istop] = (0, 1, 1, 1, 1, 1)
|
c[istart:istop] = (1, 1, 1, 1, 1, 0)
|
||||||
|
|
||||||
return x, y, c
|
return x, y, c
|
||||||
|
|
||||||
|
@ -182,12 +182,14 @@ class BarItems(pg.GraphicsObject):
|
||||||
# scene: 'QGraphicsScene', # noqa
|
# scene: 'QGraphicsScene', # noqa
|
||||||
plotitem: 'pg.PlotItem', # noqa
|
plotitem: 'pg.PlotItem', # noqa
|
||||||
pen_color: str = 'bracket',
|
pen_color: str = 'bracket',
|
||||||
|
last_bar_color: str = 'bracket',
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# XXX: for the mega-lulz increasing width here increases draw latency...
|
# XXX: for the mega-lulz increasing width here increases draw
|
||||||
# so probably don't do it until we figure that out.
|
# latency... so probably don't do it until we figure that out.
|
||||||
self.bars_pen = pg.mkPen(hcolor(pen_color), width=1)
|
self.bars_pen = pg.mkPen(hcolor(pen_color), width=1)
|
||||||
|
self.last_bar_pen = pg.mkPen(hcolor(last_bar_color), width=2)
|
||||||
|
|
||||||
# NOTE: this prevents redraws on mouse interaction which is
|
# NOTE: this prevents redraws on mouse interaction which is
|
||||||
# a huge boon for avg interaction latency.
|
# a huge boon for avg interaction latency.
|
||||||
|
@ -354,30 +356,6 @@ class BarItems(pg.GraphicsObject):
|
||||||
if flip_cache:
|
if flip_cache:
|
||||||
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||||
|
|
||||||
def paint(
|
|
||||||
self,
|
|
||||||
p: QtGui.QPainter,
|
|
||||||
opt: QtWidgets.QStyleOptionGraphicsItem,
|
|
||||||
w: QtWidgets.QWidget
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
|
|
||||||
|
|
||||||
# p.setCompositionMode(0)
|
|
||||||
p.setPen(self.bars_pen)
|
|
||||||
|
|
||||||
# TODO: one thing we could try here is pictures being drawn of
|
|
||||||
# a fixed count of bars such that based on the viewbox indices we
|
|
||||||
# only draw the "rounded up" number of "pictures worth" of bars
|
|
||||||
# as is necesarry for what's in "view". Not sure if this will
|
|
||||||
# lead to any perf gains other then when zoomed in to less bars
|
|
||||||
# in view.
|
|
||||||
p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
|
|
||||||
profiler('draw last bar')
|
|
||||||
|
|
||||||
p.drawPath(self.path)
|
|
||||||
profiler('draw history path')
|
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
# Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect
|
# Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect
|
||||||
|
|
||||||
|
@ -421,3 +399,28 @@ class BarItems(pg.GraphicsObject):
|
||||||
)
|
)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def paint(
|
||||||
|
self,
|
||||||
|
p: QtGui.QPainter,
|
||||||
|
opt: QtWidgets.QStyleOptionGraphicsItem,
|
||||||
|
w: QtWidgets.QWidget
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
|
||||||
|
|
||||||
|
# p.setCompositionMode(0)
|
||||||
|
|
||||||
|
# TODO: one thing we could try here is pictures being drawn of
|
||||||
|
# a fixed count of bars such that based on the viewbox indices we
|
||||||
|
# only draw the "rounded up" number of "pictures worth" of bars
|
||||||
|
# as is necesarry for what's in "view". Not sure if this will
|
||||||
|
# lead to any perf gains other then when zoomed in to less bars
|
||||||
|
# in view.
|
||||||
|
p.setPen(self.last_bar_pen)
|
||||||
|
p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
|
||||||
|
profiler('draw last bar')
|
||||||
|
|
||||||
|
p.setPen(self.bars_pen)
|
||||||
|
p.drawPath(self.path)
|
||||||
|
profiler('draw history path')
|
||||||
|
|
|
@ -49,7 +49,7 @@ from PyQt5 import QtCore
|
||||||
from PyQt5 import QtWidgets
|
from PyQt5 import QtWidgets
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
Qt,
|
Qt,
|
||||||
# QSize,
|
QSize,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
QItemSelectionModel,
|
QItemSelectionModel,
|
||||||
)
|
)
|
||||||
|
@ -112,6 +112,7 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
model = QStandardItemModel(self)
|
model = QStandardItemModel(self)
|
||||||
self.labels = labels
|
self.labels = labels
|
||||||
|
self._last_window_h: Optional[int] = None
|
||||||
|
|
||||||
# a std "tabular" config
|
# a std "tabular" config
|
||||||
self.setItemDelegate(FontScaledDelegate(self))
|
self.setItemDelegate(FontScaledDelegate(self))
|
||||||
|
@ -126,6 +127,10 @@ class CompleterView(QTreeView):
|
||||||
# self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
|
# self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
|
||||||
|
|
||||||
# ux settings
|
# ux settings
|
||||||
|
self.setSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Expanding,
|
||||||
|
QtWidgets.QSizePolicy.Expanding,
|
||||||
|
)
|
||||||
self.setItemsExpandable(True)
|
self.setItemsExpandable(True)
|
||||||
self.setExpandsOnDoubleClick(False)
|
self.setExpandsOnDoubleClick(False)
|
||||||
self.setAnimated(False)
|
self.setAnimated(False)
|
||||||
|
@ -153,23 +158,40 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
self.setStyleSheet(f"font: {size}px")
|
self.setStyleSheet(f"font: {size}px")
|
||||||
|
|
||||||
def resize(self):
|
#def resizeEvent(self, event: 'QEvent') -> None:
|
||||||
|
# self.resize_to_results()
|
||||||
|
# super().resizeEvent(event)
|
||||||
|
|
||||||
|
def resize_to_results(self):
|
||||||
model = self.model()
|
model = self.model()
|
||||||
cols = model.columnCount()
|
cols = model.columnCount()
|
||||||
|
|
||||||
for i in range(cols):
|
for i in range(cols):
|
||||||
self.resizeColumnToContents(i)
|
self.resizeColumnToContents(i)
|
||||||
|
|
||||||
# inclusive of search bar and header "rows" in pixel terms
|
|
||||||
rows = 100
|
|
||||||
# max_rows = 8 # 6 + search and headers
|
|
||||||
row_px = self.rowHeight(self.currentIndex())
|
row_px = self.rowHeight(self.currentIndex())
|
||||||
# print(f'font_h: {font_h}\n px_height: {px_height}')
|
|
||||||
|
|
||||||
# TODO: probably make this more general / less hacky
|
# TODO: probably make this more general / less hacky
|
||||||
|
# we should figure out the exact number of rows to allow
|
||||||
|
# inclusive of search bar and header "rows", in pixel terms.
|
||||||
|
window_h = self.window().height()
|
||||||
|
rows = round(window_h * 0.5 / row_px) - 4
|
||||||
|
|
||||||
|
# TODO: the problem here is that this view widget is **not** resizing/scaling
|
||||||
|
# when the parent layout is adjusted, not sure what exactly is up...
|
||||||
|
# only "scale up" the results view when the window size has increased/
|
||||||
|
if not self._last_window_h or self._last_window_h < window_h:
|
||||||
|
self.setMaximumSize(self.width(), rows * row_px)
|
||||||
self.setMinimumSize(self.width(), rows * row_px)
|
self.setMinimumSize(self.width(), rows * row_px)
|
||||||
self.setMaximumSize(self.width() + 10, rows * row_px)
|
|
||||||
|
#elif not self._last_window_h or self._last_window_h > window_h:
|
||||||
|
# self.setMinimumSize(self.width(), rows * row_px)
|
||||||
|
# self.setMaximumSize(self.width(), rows * row_px)
|
||||||
|
|
||||||
|
self.resize(self.width(), rows * row_px)
|
||||||
|
self._last_window_h = window_h
|
||||||
self.setFixedWidth(333)
|
self.setFixedWidth(333)
|
||||||
|
self.update()
|
||||||
|
|
||||||
def is_selecting_d1(self) -> bool:
|
def is_selecting_d1(self) -> bool:
|
||||||
cidx = self.selectionModel().currentIndex()
|
cidx = self.selectionModel().currentIndex()
|
||||||
|
@ -334,7 +356,7 @@ class CompleterView(QTreeView):
|
||||||
else:
|
else:
|
||||||
model.setItem(idx.row(), 1, QStandardItem())
|
model.setItem(idx.row(), 1, QStandardItem())
|
||||||
|
|
||||||
self.resize()
|
self.resize_to_results()
|
||||||
|
|
||||||
return idx
|
return idx
|
||||||
else:
|
else:
|
||||||
|
@ -404,7 +426,7 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
def show_matches(self) -> None:
|
def show_matches(self) -> None:
|
||||||
self.show()
|
self.show()
|
||||||
self.resize()
|
self.resize_to_results()
|
||||||
|
|
||||||
|
|
||||||
class SearchBar(Edit):
|
class SearchBar(Edit):
|
||||||
|
@ -457,7 +479,7 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
# size it as we specify
|
# size it as we specify
|
||||||
self.setSizePolicy(
|
self.setSizePolicy(
|
||||||
QtWidgets.QSizePolicy.Fixed,
|
QtWidgets.QSizePolicy.Fixed,
|
||||||
QtWidgets.QSizePolicy.Fixed,
|
QtWidgets.QSizePolicy.Expanding,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.godwidget = godwidget
|
self.godwidget = godwidget
|
||||||
|
|
|
@ -110,7 +110,7 @@ class DpiAwareFont:
|
||||||
|
|
||||||
mx_dpi = max(pdpi, ldpi)
|
mx_dpi = max(pdpi, ldpi)
|
||||||
mn_dpi = min(pdpi, ldpi)
|
mn_dpi = min(pdpi, ldpi)
|
||||||
scale = round(ldpi/pdpi)
|
scale = round(ldpi/pdpi, ndigits=2)
|
||||||
|
|
||||||
if mx_dpi <= 97: # for low dpi use larger font sizes
|
if mx_dpi <= 97: # for low dpi use larger font sizes
|
||||||
inches = _font_sizes['lo'][self._font_size]
|
inches = _font_sizes['lo'][self._font_size]
|
||||||
|
@ -121,17 +121,29 @@ class DpiAwareFont:
|
||||||
dpi = mn_dpi
|
dpi = mn_dpi
|
||||||
|
|
||||||
# dpi is likely somewhat scaled down so use slightly larger font size
|
# dpi is likely somewhat scaled down so use slightly larger font size
|
||||||
if scale > 1 and self._font_size:
|
if scale >= 1.1 and self._font_size:
|
||||||
|
|
||||||
|
if 1.2 <= scale:
|
||||||
|
inches *= (1 / scale) * 1.0616
|
||||||
|
|
||||||
|
if scale < 1.4 or scale >= 1.5:
|
||||||
# TODO: this denominator should probably be determined from
|
# TODO: this denominator should probably be determined from
|
||||||
# relative aspect ratios or something?
|
# relative aspect ratios or something?
|
||||||
inches = inches * (1 / scale) * (1 + 6/16)
|
inches = inches * (1 + 6/16)
|
||||||
|
|
||||||
dpi = mx_dpi
|
dpi = mx_dpi
|
||||||
|
log.info(f'USING MAX DPI {dpi}')
|
||||||
|
|
||||||
|
# TODO: we might want to fiddle with incrementing font size by
|
||||||
|
# +1 for the edge cases above. it seems doing it via scaling is
|
||||||
|
# always going to hit that error in range mapping from inches:
|
||||||
|
# float to px size: int.
|
||||||
self._font_inches = inches
|
self._font_inches = inches
|
||||||
|
|
||||||
font_size = math.floor(inches * dpi)
|
font_size = math.floor(inches * dpi)
|
||||||
log.debug(
|
|
||||||
f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}"
|
log.info(
|
||||||
|
f"screen:{screen.name()}]\n"
|
||||||
|
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
|
||||||
f"\nOur best guess font size is {font_size}\n"
|
f"\nOur best guess font size is {font_size}\n"
|
||||||
)
|
)
|
||||||
# apply the size
|
# apply the size
|
||||||
|
@ -205,19 +217,26 @@ def hcolor(name: str) -> str:
|
||||||
'svags': '#0a0e14',
|
'svags': '#0a0e14',
|
||||||
|
|
||||||
# fifty shades
|
# fifty shades
|
||||||
|
'original': '#a9a9a9',
|
||||||
'gray': '#808080', # like the kick
|
'gray': '#808080', # like the kick
|
||||||
'grayer': '#4c4c4c',
|
'grayer': '#4c4c4c',
|
||||||
'grayest': '#3f3f3f',
|
'grayest': '#3f3f3f',
|
||||||
'i3': '#494D4F',
|
|
||||||
'jet': '#343434',
|
|
||||||
'cadet': '#91A3B0',
|
'cadet': '#91A3B0',
|
||||||
'marengo': '#91A3B0',
|
'marengo': '#91A3B0',
|
||||||
'charcoal': '#36454F',
|
|
||||||
'gunmetal': '#91A3B0',
|
'gunmetal': '#91A3B0',
|
||||||
'battleship': '#848482',
|
'battleship': '#848482',
|
||||||
'davies': '#555555',
|
|
||||||
|
# bluish
|
||||||
|
'charcoal': '#36454F',
|
||||||
|
|
||||||
|
# default bars
|
||||||
'bracket': '#666666', # like the logo
|
'bracket': '#666666', # like the logo
|
||||||
'original': '#a9a9a9',
|
|
||||||
|
# work well for filled polygons which want a 'bracket' feel
|
||||||
|
# going light to dark
|
||||||
|
'davies': '#555555',
|
||||||
|
'i3': '#494D4F',
|
||||||
|
'jet': '#343434',
|
||||||
|
|
||||||
# from ``qdarkstyle`` palette
|
# from ``qdarkstyle`` palette
|
||||||
'default_darkest': DarkPalette.COLOR_BACKGROUND_1,
|
'default_darkest': DarkPalette.COLOR_BACKGROUND_1,
|
||||||
|
|
|
@ -151,8 +151,8 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
# XXX: for tiling wms this should scale
|
# XXX: for tiling wms this should scale
|
||||||
# with the alloted window size.
|
# with the alloted window size.
|
||||||
# TODO: detect for tiling and if untrue set some size?
|
# TODO: detect for tiling and if untrue set some size?
|
||||||
# size = (300, 500)
|
size = (300, 500)
|
||||||
size = (0, 0)
|
#size = (0, 0)
|
||||||
|
|
||||||
title = 'piker chart (ur symbol is loading bby)'
|
title = 'piker chart (ur symbol is loading bby)'
|
||||||
|
|
||||||
|
@ -163,6 +163,7 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
|
|
||||||
self._status_bar: QStatusBar = None
|
self._status_bar: QStatusBar = None
|
||||||
self._status_label: QLabel = None
|
self._status_label: QLabel = None
|
||||||
|
self._size: Optional[tuple[int, int]] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mode_label(self) -> QtGui.QLabel:
|
def mode_label(self) -> QtGui.QLabel:
|
||||||
|
@ -267,6 +268,22 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
assert screen, "Wow Qt is dumb as shit and has no screen..."
|
assert screen, "Wow Qt is dumb as shit and has no screen..."
|
||||||
return screen
|
return screen
|
||||||
|
|
||||||
|
def configure_to_desktop(
|
||||||
|
self,
|
||||||
|
size: Optional[tuple[int, int]] = None,
|
||||||
|
) -> None:
|
||||||
|
# https://stackoverflow.com/a/18975846
|
||||||
|
if not size and not self._size:
|
||||||
|
app = QtGui.QApplication.instance()
|
||||||
|
geo = self.current_screen().geometry()
|
||||||
|
h, w = geo.height(), geo.width()
|
||||||
|
self.setMaximumSize(w, h)
|
||||||
|
# use approx 1/3 of the area of the screen by default
|
||||||
|
self._size = round(w * .666), round(h * .666)
|
||||||
|
|
||||||
|
self.resize(*size or self._size)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# singleton app per actor
|
# singleton app per actor
|
||||||
_qt_win: QtGui.QMainWindow = None
|
_qt_win: QtGui.QMainWindow = None
|
||||||
|
|
|
@ -22,6 +22,7 @@ from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
import platform
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Dict, Callable, Any
|
from typing import Optional, Dict, Callable, Any
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -429,6 +430,7 @@ class OrderMode:
|
||||||
|
|
||||||
# TODO: make this not trash.
|
# TODO: make this not trash.
|
||||||
# XXX: linux only for now
|
# XXX: linux only for now
|
||||||
|
if platform.system() != "Windows":
|
||||||
result = await trio.run_process(
|
result = await trio.run_process(
|
||||||
[
|
[
|
||||||
'notify-send',
|
'notify-send',
|
||||||
|
|
|
@ -25,6 +25,8 @@ import i3ipc
|
||||||
i3 = i3ipc.Connection()
|
i3 = i3ipc.Connection()
|
||||||
t = i3.get_tree()
|
t = i3.get_tree()
|
||||||
|
|
||||||
|
orig_win_id = t.find_focused().window
|
||||||
|
|
||||||
# for tws
|
# for tws
|
||||||
win_names: list[str] = [
|
win_names: list[str] = [
|
||||||
'Interactive Brokers', # tws running in i3
|
'Interactive Brokers', # tws running in i3
|
||||||
|
@ -51,11 +53,20 @@ for name in win_names:
|
||||||
|
|
||||||
# move mouse to bottom left of window (where there should
|
# move mouse to bottom left of window (where there should
|
||||||
# be nothing to click).
|
# be nothing to click).
|
||||||
'mousemove_relative', '--sync', str(w-3), str(h-3),
|
'mousemove_relative', '--sync', str(w-4), str(h-4),
|
||||||
|
|
||||||
# NOTE: we may need to stick a `--retry 3` in here..
|
# NOTE: we may need to stick a `--retry 3` in here..
|
||||||
'click', '--window', win_id, '1',
|
'click', '--window', win_id, '--repeat', '3', '1',
|
||||||
|
|
||||||
# hackzorzes
|
# hackzorzes
|
||||||
'key', 'ctrl+alt+f',
|
'key', 'ctrl+alt+f',
|
||||||
])
|
],
|
||||||
|
timeout=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# re-activate and focus original window
|
||||||
|
subprocess.call([
|
||||||
|
'xdotool',
|
||||||
|
'windowactivate', '--sync', str(orig_win_id),
|
||||||
|
'click', '--window', str(orig_win_id), '1',
|
||||||
|
])
|
||||||
|
|
Loading…
Reference in New Issue