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 .
|
||||
|
||||
|
||||
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
|
||||
****************
|
||||
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.
|
||||
|
||||
"""
|
||||
# further examples of interest:
|
||||
# https://gist.github.com/njsmith/cf6fc0a97f53865f2c671659c88c1798#file-cache-py-L8
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import (
|
||||
Any,
|
||||
Hashable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
AsyncContextManager,
|
||||
)
|
||||
from contextlib import (
|
||||
asynccontextmanager,
|
||||
)
|
||||
|
||||
import trio
|
||||
from trio_typing import TaskStatus
|
||||
import tractor
|
||||
from tractor.trionics import maybe_open_context
|
||||
|
||||
from .brokers import get_brokermod
|
||||
from .log import get_logger
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
|
@ -74,112 +62,6 @@ def async_lifo_cache(maxsize=128):
|
|||
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
|
||||
async def open_cached_client(
|
||||
brokername: str,
|
||||
|
@ -190,7 +72,7 @@ async def open_cached_client(
|
|||
|
||||
'''
|
||||
brokermod = get_brokermod(brokername)
|
||||
async with maybe_open_ctx(
|
||||
async with maybe_open_context(
|
||||
key=brokername,
|
||||
mngr=brokermod.get_client(),
|
||||
) as (cache_hit, client):
|
||||
|
|
|
@ -21,7 +21,7 @@ Profiling wrappers for internal libs.
|
|||
import time
|
||||
from functools import wraps
|
||||
|
||||
_pg_profile: bool = False
|
||||
_pg_profile: bool = True
|
||||
|
||||
|
||||
def pg_profile_enabled() -> bool:
|
||||
|
|
|
@ -19,7 +19,7 @@ Binance backend
|
|||
|
||||
"""
|
||||
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 trio
|
||||
|
@ -37,7 +37,7 @@ from .._cacheables import open_cached_client
|
|||
from ._util import resproc, SymbolNotFound
|
||||
from ..log import get_logger, get_console_log
|
||||
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__)
|
||||
|
||||
|
@ -295,7 +295,7 @@ class AggTrade(BaseModel):
|
|||
M: bool # Ignore
|
||||
|
||||
|
||||
async def stream_messages(ws):
|
||||
async def stream_messages(ws: NoBsWs) -> AsyncGenerator[NoBsWs, dict]:
|
||||
|
||||
timeouts = 0
|
||||
while True:
|
||||
|
@ -487,11 +487,20 @@ async def stream_quotes(
|
|||
# signal to caller feed is ready for consumption
|
||||
feed_is_live.set()
|
||||
|
||||
# import time
|
||||
# last = time.time()
|
||||
|
||||
# start streaming
|
||||
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()
|
||||
await send_chan.send({topic: msg})
|
||||
# last = time.time()
|
||||
|
||||
|
||||
@tractor.context
|
||||
|
|
|
@ -1157,6 +1157,11 @@ async def backfill_bars(
|
|||
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)
|
||||
if out is None:
|
||||
raise RuntimeError("Could not pull currrent history?!")
|
||||
|
|
|
@ -43,11 +43,15 @@ def humanize(
|
|||
if not number or number <= 0:
|
||||
return round(number, ndigits=digits)
|
||||
|
||||
mag = math.floor(math.log(number, 10))
|
||||
mag = round(math.log(number, 10))
|
||||
if mag < 3:
|
||||
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(
|
||||
value=round(number/10**maxmag, ndigits=digits),
|
||||
|
|
|
@ -20,6 +20,7 @@ In da suit parlances: "Execution management systems"
|
|||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from math import isnan
|
||||
from pprint import pformat
|
||||
import time
|
||||
from typing import AsyncIterator, Callable
|
||||
|
@ -47,9 +48,11 @@ log = get_logger(__name__)
|
|||
|
||||
# TODO: numba all of this
|
||||
def mk_check(
|
||||
|
||||
trigger_price: float,
|
||||
known_last: float,
|
||||
action: str,
|
||||
|
||||
) -> Callable[[float, float], bool]:
|
||||
"""Create a predicate for given ``exec_price`` based on last known
|
||||
price, ``known_last``.
|
||||
|
@ -77,8 +80,7 @@ def mk_check(
|
|||
|
||||
return check_lt
|
||||
|
||||
else:
|
||||
return None
|
||||
raise ValueError('trigger: {trigger_price}, last: {known_last}')
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -177,7 +179,15 @@ async def clear_dark_triggers(
|
|||
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
|
||||
continue
|
||||
|
||||
|
@ -1005,7 +1015,8 @@ async def _emsd_main(
|
|||
first_quote = feed.first_quotes[symbol]
|
||||
|
||||
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
|
||||
# flow dialogue
|
||||
|
@ -1035,7 +1046,7 @@ async def _emsd_main(
|
|||
|
||||
# signal to client that we're started and deliver
|
||||
# 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
|
||||
# begin handling inbound order requests and updates
|
||||
|
|
|
@ -60,7 +60,7 @@ def repodir():
|
|||
"""
|
||||
dirpath = os.path.abspath(
|
||||
# we're 3 levels down in **this** module file
|
||||
dirname(dirname(dirname(os.path.realpath(__file__))))
|
||||
dirname(dirname(os.path.realpath(__file__)))
|
||||
)
|
||||
return dirpath
|
||||
|
||||
|
@ -73,7 +73,7 @@ def load(
|
|||
path = path or get_broker_conf_path()
|
||||
if not os.path.isfile(path):
|
||||
shutil.copyfile(
|
||||
os.path.join(repodir(), 'data/brokers.toml'),
|
||||
os.path.join(repodir(), 'config', 'brokers.toml'),
|
||||
path,
|
||||
)
|
||||
|
||||
|
|
|
@ -313,7 +313,8 @@ async def uniform_rate_send(
|
|||
|
||||
except trio.WouldBlock:
|
||||
now = time.time()
|
||||
rate = 1 / (now - last_send)
|
||||
diff = now - last_send
|
||||
rate = 1 / diff if diff else float('inf')
|
||||
last_send = now
|
||||
|
||||
# log.info(f'{rate} Hz sending quotes') # \n{first_quote}')
|
||||
|
|
|
@ -272,9 +272,8 @@ class ShmArray:
|
|||
return end
|
||||
|
||||
except ValueError as err:
|
||||
# shoudl raise if diff detected
|
||||
# should raise if diff detected
|
||||
self.diff_err_fields(data)
|
||||
|
||||
raise err
|
||||
|
||||
def diff_err_fields(
|
||||
|
@ -395,6 +394,7 @@ def open_shm_array(
|
|||
|
||||
# "unlink" created shm on process teardown by
|
||||
# pushing teardown calls onto actor context stack
|
||||
|
||||
tractor._actor._lifetime_stack.callback(shmarr.close)
|
||||
tractor._actor._lifetime_stack.callback(shmarr.destroy)
|
||||
|
||||
|
|
|
@ -133,9 +133,11 @@ def mk_symbol(
|
|||
|
||||
|
||||
def from_df(
|
||||
|
||||
df: pd.DataFrame,
|
||||
source=None,
|
||||
default_tf=None
|
||||
|
||||
) -> np.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 types import ModuleType
|
||||
from typing import Any, Callable
|
||||
from typing import Any, Callable, AsyncGenerator
|
||||
import json
|
||||
|
||||
import trio
|
||||
|
@ -127,7 +127,7 @@ async def open_autorecon_ws(
|
|||
|
||||
# TODO: proper type annot smh
|
||||
fixture: Callable,
|
||||
):
|
||||
) -> AsyncGenerator[tuple[...], NoBsWs]:
|
||||
"""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_typing import TaskStatus
|
||||
import tractor
|
||||
# from tractor import _broadcast
|
||||
from pydantic import BaseModel
|
||||
|
||||
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 .._daemon import (
|
||||
maybe_spawn_brokerd,
|
||||
|
@ -247,7 +246,7 @@ async def allocate_persistent_feed(
|
|||
|
||||
|
||||
@tractor.context
|
||||
async def attach_feed_bus(
|
||||
async def open_feed_bus(
|
||||
|
||||
ctx: tractor.Context,
|
||||
brokername: str,
|
||||
|
@ -364,7 +363,7 @@ async def open_sample_step_stream(
|
|||
# XXX: this should be singleton on a host,
|
||||
# a lone broker-daemon per provider should be
|
||||
# created for all practical purposes
|
||||
async with maybe_open_ctx(
|
||||
async with maybe_open_context(
|
||||
key=delay_s,
|
||||
mngr=portal.open_stream_from(
|
||||
iter_ohlc_periods,
|
||||
|
@ -507,7 +506,7 @@ async def open_feed(
|
|||
|
||||
portal.open_context(
|
||||
|
||||
attach_feed_bus,
|
||||
open_feed_bus,
|
||||
brokername=brokername,
|
||||
symbol=sym,
|
||||
loglevel=loglevel,
|
||||
|
@ -586,7 +585,7 @@ async def maybe_open_feed(
|
|||
'''
|
||||
sym = symbols[0].lower()
|
||||
|
||||
async with maybe_open_ctx(
|
||||
async with maybe_open_context(
|
||||
key=(brokername, sym),
|
||||
mngr=open_feed(
|
||||
brokername,
|
||||
|
|
|
@ -34,7 +34,7 @@ from ..data import attach_shm_array
|
|||
from ..data.feed import Feed
|
||||
from ..data._sharedmem import ShmArray
|
||||
from ._momo import _rsi, _wma
|
||||
from ._volume import _tina_vwap
|
||||
from ._volume import _tina_vwap, dolla_vlm
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
@ -42,6 +42,7 @@ _fsp_builtins = {
|
|||
'rsi': _rsi,
|
||||
'wma': _wma,
|
||||
'vwap': _tina_vwap,
|
||||
'dolla_vlm': dolla_vlm,
|
||||
}
|
||||
|
||||
# TODO: things to figure the heck out:
|
||||
|
@ -90,7 +91,7 @@ async def fsp_compute(
|
|||
func_name: str,
|
||||
func: Callable,
|
||||
|
||||
attach_stream: bool = True,
|
||||
attach_stream: bool = False,
|
||||
task_status: TaskStatus[None] = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
) -> None:
|
||||
|
@ -144,10 +145,13 @@ async def fsp_compute(
|
|||
profiler(f'{func_name} pushed history')
|
||||
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
|
||||
with trio.CancelScope() as cs:
|
||||
tracker = TaskTracker(trio.Event(), cs)
|
||||
await ctx.started(index)
|
||||
task_status.started((tracker, 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
|
||||
# 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
|
||||
from tractor.trionics._broadcast import AsyncReceiver
|
||||
|
||||
from ..data._normalize import iterticks
|
||||
from ..data._sharedmem import ShmArray
|
||||
|
||||
|
||||
def wap(
|
||||
|
||||
signal: np.ndarray,
|
||||
weights: np.ndarray,
|
||||
|
||||
) -> np.ndarray:
|
||||
"""Weighted average price from signal and weights.
|
||||
|
||||
|
@ -47,15 +51,22 @@ def wap(
|
|||
|
||||
|
||||
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,
|
||||
) -> 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.
|
||||
|
||||
"""
|
||||
'''
|
||||
if anchors is None:
|
||||
# TODO:
|
||||
# anchor to session start of data if possible
|
||||
|
@ -75,7 +86,6 @@ async def _tina_vwap(
|
|||
# vwap_tot = h_vwap[-1]
|
||||
|
||||
async for quote in source:
|
||||
|
||||
for tick in iterticks(quote, types=['trade']):
|
||||
|
||||
# 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 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()
|
||||
|
||||
# configure graphics update throttling based on display refresh rate
|
||||
_display._clear_throttle_rate = min(
|
||||
_display._quote_throttle_rate = min(
|
||||
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
|
||||
# _style.style_ze_sheets(godwidget)
|
||||
|
|
|
@ -25,6 +25,9 @@ from PyQt5.QtCore import Qt
|
|||
from PyQt5.QtWidgets import (
|
||||
QFrame,
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QSplitter,
|
||||
# QSizePolicy,
|
||||
)
|
||||
import numpy as np
|
||||
|
@ -53,6 +56,7 @@ from ._style import (
|
|||
)
|
||||
from ..data.feed import Feed
|
||||
from ..data._source import Symbol
|
||||
from ..data._sharedmem import ShmArray
|
||||
from ..log import get_logger
|
||||
from ._interaction import ChartView
|
||||
from ._forms import FieldsForm
|
||||
|
@ -64,11 +68,11 @@ log = get_logger(__name__)
|
|||
class GodWidget(QWidget):
|
||||
'''
|
||||
"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
|
||||
organizing lower level charts as well as other widgets used to
|
||||
control or modify them.
|
||||
organizing charts as well as other sub-widgets used to control or
|
||||
modify them.
|
||||
|
||||
'''
|
||||
def __init__(
|
||||
|
@ -80,19 +84,19 @@ class GodWidget(QWidget):
|
|||
|
||||
super().__init__(parent)
|
||||
|
||||
self.hbox = QtWidgets.QHBoxLayout(self)
|
||||
self.hbox = QHBoxLayout(self)
|
||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.hbox.setSpacing(6)
|
||||
self.hbox.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.vbox = QtWidgets.QVBoxLayout()
|
||||
self.vbox = QVBoxLayout()
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.vbox.setSpacing(2)
|
||||
self.vbox.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.hbox.addLayout(self.vbox)
|
||||
|
||||
# self.toolbar_layout = QtWidgets.QHBoxLayout()
|
||||
# self.toolbar_layout = QHBoxLayout()
|
||||
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# self.vbox.addLayout(self.toolbar_layout)
|
||||
|
||||
|
@ -106,25 +110,8 @@ class GodWidget(QWidget):
|
|||
# assigned in the startup func `_async_main()`
|
||||
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):
|
||||
# self.tf_layout = QtWidgets.QHBoxLayout()
|
||||
# self.tf_layout = QHBoxLayout()
|
||||
# self.tf_layout.setSpacing(0)
|
||||
# self.tf_layout.setContentsMargins(0, 12, 0, 0)
|
||||
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
|
||||
|
@ -145,6 +132,23 @@ class GodWidget(QWidget):
|
|||
# self.strategy_box = StrategyBoxWidget(self)
|
||||
# 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(
|
||||
self,
|
||||
|
||||
|
@ -255,7 +259,7 @@ class ChartnPane(QFrame):
|
|||
|
||||
'''
|
||||
sidepane: FieldsForm
|
||||
hbox: QtWidgets.QHBoxLayout
|
||||
hbox: QHBoxLayout
|
||||
chart: Optional['ChartPlotWidget'] = None
|
||||
|
||||
def __init__(
|
||||
|
@ -271,7 +275,7 @@ class ChartnPane(QFrame):
|
|||
self.sidepane = sidepane
|
||||
self.chart = None
|
||||
|
||||
hbox = self.hbox = QtWidgets.QHBoxLayout(self)
|
||||
hbox = self.hbox = QHBoxLayout(self)
|
||||
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||
hbox.setContentsMargins(0, 0, 0, 0)
|
||||
hbox.setSpacing(3)
|
||||
|
@ -281,21 +285,14 @@ class ChartnPane(QFrame):
|
|||
|
||||
class LinkedSplits(QWidget):
|
||||
'''
|
||||
Widget that holds a central chart plus derived
|
||||
subcharts computed from the original data set apart
|
||||
by splitters for resizing.
|
||||
Composite that holds a central chart plus a set of (derived)
|
||||
subcharts (usually computed from the original data) arranged in
|
||||
a splitter for resizing.
|
||||
|
||||
A single internal references to the data is maintained
|
||||
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__(
|
||||
|
||||
self,
|
||||
|
@ -325,11 +322,11 @@ class LinkedSplits(QWidget):
|
|||
# self.xaxis_ind.setStyle(showValues=False)
|
||||
# self.xaxis.hide()
|
||||
|
||||
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
|
||||
self.splitter.setMidLineWidth(1)
|
||||
self.splitter.setHandleWidth(0)
|
||||
self.splitter = QSplitter(QtCore.Qt.Vertical)
|
||||
self.splitter.setMidLineWidth(0)
|
||||
self.splitter.setHandleWidth(2)
|
||||
|
||||
self.layout = QtWidgets.QVBoxLayout(self)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.splitter)
|
||||
|
||||
|
@ -341,20 +338,28 @@ class LinkedSplits(QWidget):
|
|||
|
||||
def set_split_sizes(
|
||||
self,
|
||||
# prop: float = 0.375, # proportion allocated to consumer subcharts
|
||||
prop: float = 5/8,
|
||||
prop: Optional[float] = None,
|
||||
|
||||
) -> None:
|
||||
'''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
|
||||
min_h_ind = int((self.height() * prop) / len(self.subplots))
|
||||
min_h_ind = int((self.height() * prop) / ln)
|
||||
|
||||
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:
|
||||
if self.chart is not None:
|
||||
|
@ -374,16 +379,21 @@ class LinkedSplits(QWidget):
|
|||
style: str = 'bar',
|
||||
|
||||
) -> '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.
|
||||
"""
|
||||
|
||||
'''
|
||||
# add crosshairs
|
||||
self.cursor = Cursor(
|
||||
linkedsplits=self,
|
||||
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(
|
||||
|
||||
name=symbol.key,
|
||||
|
@ -425,9 +435,7 @@ class LinkedSplits(QWidget):
|
|||
**cpw_kwargs,
|
||||
|
||||
) -> 'ChartPlotWidget':
|
||||
'''Add (sub)plots to chart widget by name.
|
||||
|
||||
If ``name`` == ``"main"`` the chart will be the the primary view.
|
||||
'''Add (sub)plots to chart widget by key.
|
||||
|
||||
'''
|
||||
if self.chart is None and not _is_main:
|
||||
|
@ -495,8 +503,9 @@ class LinkedSplits(QWidget):
|
|||
cpw.plotItem.vb.linkedsplits = self
|
||||
cpw.setFrameStyle(
|
||||
QtWidgets.QFrame.StyledPanel
|
||||
# | QtWidgets.QFrame.Plain)
|
||||
# | QtWidgets.QFrame.Plain
|
||||
)
|
||||
|
||||
cpw.hideButtons()
|
||||
|
||||
# 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)
|
||||
|
||||
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:
|
||||
raise ValueError(f"Chart style {style} is currently unsupported")
|
||||
|
@ -523,14 +547,7 @@ class LinkedSplits(QWidget):
|
|||
if not _is_main:
|
||||
# track by name
|
||||
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)
|
||||
|
||||
# scale split regions
|
||||
self.set_split_sizes()
|
||||
|
||||
|
@ -586,6 +603,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
view_color: str = 'papas_special',
|
||||
pen_color: str = 'bracket',
|
||||
|
||||
# TODO: load from config
|
||||
use_open_gl: bool = False,
|
||||
|
||||
static_yrange: Optional[tuple[float, float]] = None,
|
||||
|
||||
**kwargs,
|
||||
|
@ -600,9 +620,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# parent=None,
|
||||
# plotItem=None,
|
||||
# antialias=True,
|
||||
useOpenGL=True,
|
||||
**kwargs
|
||||
)
|
||||
self.useOpenGL(use_open_gl)
|
||||
self.name = name
|
||||
self.data_key = data_key
|
||||
self.linked = linkedsplits
|
||||
|
@ -619,7 +639,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
'ohlc': array,
|
||||
}
|
||||
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] = {}
|
||||
|
||||
|
@ -732,6 +753,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self._vb.setXRange(
|
||||
min=l + 1,
|
||||
max=r + 1,
|
||||
|
||||
# TODO: holy shit, wtf dude... why tf would this not be 0 by
|
||||
# default... speechless.
|
||||
padding=0,
|
||||
|
@ -772,7 +794,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
update_func=ContentsLabel.update_from_ohlc,
|
||||
)
|
||||
|
||||
self._add_sticky(name)
|
||||
self._add_sticky(name, bg_color='davies')
|
||||
|
||||
return graphics
|
||||
|
||||
|
@ -784,7 +806,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
array_key: Optional[str] = None,
|
||||
overlay: bool = False,
|
||||
color: str = 'default_light',
|
||||
color: Optional[str] = None,
|
||||
add_label: bool = True,
|
||||
|
||||
**pdi_kwargs,
|
||||
|
@ -794,15 +816,18 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
the input array ``data``.
|
||||
|
||||
"""
|
||||
_pdi_defaults = {
|
||||
'pen': pg.mkPen(hcolor(color)),
|
||||
}
|
||||
pdi_kwargs.update(_pdi_defaults)
|
||||
color = color or self.pen_color or 'default_light'
|
||||
pdi_kwargs.update({
|
||||
'color': color
|
||||
})
|
||||
|
||||
data_key = array_key or name
|
||||
|
||||
# pg internals for reference.
|
||||
# curve = pg.PlotDataItem(
|
||||
# curve = pg.PlotCurveItem(
|
||||
|
||||
# yah, we wrote our own B)
|
||||
curve = FastAppendCurve(
|
||||
y=data[data_key],
|
||||
x=data['index'],
|
||||
|
@ -840,14 +865,14 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
if overlay:
|
||||
anchor_at = ('bottom', 'left')
|
||||
self._overlays.add(name)
|
||||
self._overlays[name] = None
|
||||
|
||||
else:
|
||||
anchor_at = ('top', 'left')
|
||||
|
||||
# TODO: something instead of stickies for overlays
|
||||
# (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:
|
||||
self.linked.cursor.add_curve_cursor(self, curve)
|
||||
|
@ -861,6 +886,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
return curve
|
||||
|
||||
# TODO: make this a ctx mngr
|
||||
def _add_sticky(
|
||||
self,
|
||||
|
||||
|
@ -890,46 +916,50 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
def update_ohlc_from_array(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
graphics_name: str,
|
||||
array: np.ndarray,
|
||||
**kwargs,
|
||||
) -> pg.GraphicsObject:
|
||||
"""Update the named internal graphics from ``array``.
|
||||
|
||||
"""
|
||||
) -> pg.GraphicsObject:
|
||||
'''Update the named internal graphics from ``array``.
|
||||
|
||||
'''
|
||||
self._arrays['ohlc'] = array
|
||||
graphics = self._graphics[name]
|
||||
graphics = self._graphics[graphics_name]
|
||||
graphics.update_from_array(array, **kwargs)
|
||||
return graphics
|
||||
|
||||
def update_curve_from_array(
|
||||
self,
|
||||
|
||||
name: str,
|
||||
graphics_name: str,
|
||||
array: np.ndarray,
|
||||
array_key: Optional[str] = None,
|
||||
|
||||
**kwargs,
|
||||
|
||||
) -> 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 name not in self._overlays:
|
||||
if graphics_name not in self._overlays:
|
||||
self._arrays['ohlc'] = array
|
||||
else:
|
||||
self._arrays[data_key] = array
|
||||
|
||||
curve = self._graphics[name]
|
||||
curve = self._graphics[graphics_name]
|
||||
|
||||
if len(array):
|
||||
# TODO: we should instead implement a diff based
|
||||
# "only update with new items" on the pg.PlotCurveItem
|
||||
# one place to dig around this might be the `QBackingStore`
|
||||
# https://doc.qt.io/qt-5/qbackingstore.html
|
||||
# curve.setData(y=array[name], x=array['index'], **kwargs)
|
||||
# NOTE: back when we weren't implementing the curve graphics
|
||||
# ourselves you'd have updates using this method:
|
||||
# curve.setData(y=array[graphics_name], x=array['index'], **kwargs)
|
||||
|
||||
# NOTE: graphics **must** implement a diff based update
|
||||
# operation where an internal ``FastUpdateCurve._xrange`` is
|
||||
# used to determine if the underlying path needs to be
|
||||
# pre/ap-pended.
|
||||
curve.update_from_array(
|
||||
x=array['index'],
|
||||
y=array[data_key],
|
||||
|
@ -941,16 +971,23 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
def _set_yrange(
|
||||
self,
|
||||
*,
|
||||
|
||||
yrange: Optional[tuple[float, float]] = None,
|
||||
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:
|
||||
"""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
|
||||
that data always fits nicely inside the current view of the
|
||||
data set.
|
||||
|
||||
"""
|
||||
'''
|
||||
set_range = True
|
||||
|
||||
if self._static_yrange == 'axis':
|
||||
|
@ -966,52 +1003,50 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# Determine max, min y values in viewable x-range from data.
|
||||
# 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"
|
||||
# the data set up to the point where ``_min_points_to_show``
|
||||
# are left.
|
||||
# view_len = r - l
|
||||
if autoscale_linked_plots:
|
||||
# avoid recursion by sibling plots
|
||||
linked = self.linked
|
||||
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
|
||||
# extra = view_len - _min_points_to_show
|
||||
|
||||
# begin = self._arrays['ohlc'][0]['index'] - extra
|
||||
|
||||
# # end = len(self._arrays['ohlc']) - 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
|
||||
# log.debug(
|
||||
# f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n"
|
||||
# f"view_len: {view_len}, bars_len: {bars_len}\n"
|
||||
# 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']
|
||||
ifirst = a[0]['index']
|
||||
bars = a[lbar - ifirst:rbar - ifirst + 1]
|
||||
|
||||
if not len(bars):
|
||||
# likely no data loaded yet or extreme scrolling?
|
||||
log.error(f"WTF bars_range = {lbar}:{rbar}")
|
||||
return
|
||||
|
||||
if self.data_key != self.linked.symbol.key:
|
||||
bars = a[self.data_key]
|
||||
bars = bars[self.data_key]
|
||||
ylow = np.nanmin(bars)
|
||||
yhigh = np.nanmax((bars))
|
||||
yhigh = np.nanmax(bars)
|
||||
# print(f'{(ylow, yhigh)}')
|
||||
else:
|
||||
# just the std ohlc bars
|
||||
ylow = np.nanmin(bars['low'])
|
||||
|
@ -1072,7 +1107,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# TODO: this should go onto some sort of
|
||||
# data-view strimg thinger..right?
|
||||
ohlc = self._shm.array
|
||||
# ohlc = chart._shm.array
|
||||
|
||||
# XXX: not sure why the time is so off here
|
||||
# looks like we're gonna have to do some fixing..
|
||||
|
|
|
@ -18,25 +18,105 @@
|
|||
Fast, smooth, sexy curves.
|
||||
|
||||
"""
|
||||
from typing import Tuple
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
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 ._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
|
||||
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
|
||||
# we're basically only using the pen setting now...
|
||||
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
|
||||
# interactions slower (such as zooming) and if so maybe if/when
|
||||
|
@ -46,8 +126,9 @@ class FastAppendCurve(pg.PlotCurveItem):
|
|||
|
||||
def update_from_array(
|
||||
self,
|
||||
x,
|
||||
y,
|
||||
x: np.ndarray,
|
||||
y: np.ndarray,
|
||||
|
||||
) -> QtGui.QPainterPath:
|
||||
|
||||
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
|
||||
|
@ -56,17 +137,33 @@ class FastAppendCurve(pg.PlotCurveItem):
|
|||
# print(f"xrange: {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]
|
||||
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(
|
||||
x[:-1],
|
||||
y[:-1],
|
||||
connect='all'
|
||||
x_out,
|
||||
y_out,
|
||||
connect='all',
|
||||
finiteCheck=False,
|
||||
)
|
||||
profiler('generate fresh path')
|
||||
|
||||
# if self._step_mode:
|
||||
# self.path.closeSubpath()
|
||||
|
||||
# TODO: get this working - right now it's giving heck on vwap...
|
||||
# if prepend_length:
|
||||
# breakpoint()
|
||||
|
@ -83,7 +180,16 @@ class FastAppendCurve(pg.PlotCurveItem):
|
|||
# # self.path.moveTo(new_x[0], new_y[0])
|
||||
# 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}")
|
||||
new_x = x[-append_length - 2:-1]
|
||||
new_y = y[-append_length - 2:-1]
|
||||
|
@ -92,12 +198,29 @@ class FastAppendCurve(pg.PlotCurveItem):
|
|||
append_path = pg.functions.arrayToQPath(
|
||||
new_x,
|
||||
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()}")
|
||||
# self.path.moveTo(new_x[0], new_y[0])
|
||||
# self.path.connectPath(append_path)
|
||||
self.path.connectPath(append_path)
|
||||
path.connectPath(append_path)
|
||||
|
||||
# XXX: pretty annoying but, without this there's little
|
||||
# artefacts on the append updates to the curve...
|
||||
|
@ -112,8 +235,23 @@ class FastAppendCurve(pg.PlotCurveItem):
|
|||
self.xData = x
|
||||
self.yData = y
|
||||
|
||||
self._xrange = x[0], x[-1]
|
||||
self._last_line = QtCore.QLineF(x[-2], y[-2], x[-1], y[-1])
|
||||
x0, x_last = self._xrange = x[0], x[-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
|
||||
# do update before reverting to cache mode
|
||||
|
@ -143,13 +281,13 @@ class FastAppendCurve(pg.PlotCurveItem):
|
|||
w = hb_size.width() + 1
|
||||
h = hb_size.height() + 1
|
||||
|
||||
br = QtCore.QRectF(
|
||||
br = QRectF(
|
||||
|
||||
# top left
|
||||
QtCore.QPointF(hb.topLeft()),
|
||||
QPointF(hb.topLeft()),
|
||||
|
||||
# total size
|
||||
QtCore.QSizeF(w, h)
|
||||
QSizeF(w, h)
|
||||
)
|
||||
# print(f'bounding rect: {br}')
|
||||
return br
|
||||
|
@ -164,9 +302,26 @@ class FastAppendCurve(pg.PlotCurveItem):
|
|||
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
|
||||
# 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)
|
||||
profiler('.drawLine()')
|
||||
|
||||
p.setPen(self.opts['pen'])
|
||||
p.drawPath(self.path)
|
||||
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:
|
||||
# https://bugreports.qt.io/browse/QTBUG-53022
|
||||
# it seems to work on windows.. no idea wtf is up.
|
||||
is_windows = False
|
||||
if platform.system() == "Windows":
|
||||
is_windows = True
|
||||
|
||||
# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute
|
||||
# must be set before creating the application
|
||||
|
@ -182,6 +184,8 @@ def run_qtractor(
|
|||
|
||||
window.main_widget = main_widget
|
||||
window.setCentralWidget(instance)
|
||||
if is_windows:
|
||||
window.configure_to_desktop()
|
||||
|
||||
# actually render to screen
|
||||
window.show()
|
||||
|
|
|
@ -732,7 +732,7 @@ def mk_order_pane_layout(
|
|||
|
||||
) -> FieldsForm:
|
||||
|
||||
font_size: int = _font.px_size - 1
|
||||
font_size: int = _font.px_size - 2
|
||||
|
||||
# TODO: maybe just allocate the whole fields form here
|
||||
# and expect an async ctx entry?
|
||||
|
|
|
@ -341,7 +341,14 @@ class ChartView(ViewBox):
|
|||
**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
|
||||
self.setMouseEnabled(x=True, y=False)
|
||||
|
@ -533,7 +540,6 @@ class ChartView(ViewBox):
|
|||
# self.updateScaleBox(ev.buttonDownPos(), ev.pos())
|
||||
else:
|
||||
# default bevavior: click to pan view
|
||||
|
||||
tr = self.childGroup.transform()
|
||||
tr = fn.invertQTransform(tr)
|
||||
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
|
||||
# prior bars last edge thus providing a small "gap"/"space"
|
||||
# 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
|
||||
|
||||
|
@ -182,12 +182,14 @@ class BarItems(pg.GraphicsObject):
|
|||
# scene: 'QGraphicsScene', # noqa
|
||||
plotitem: 'pg.PlotItem', # noqa
|
||||
pen_color: str = 'bracket',
|
||||
last_bar_color: str = 'bracket',
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
# XXX: for the mega-lulz increasing width here increases draw latency...
|
||||
# so probably don't do it until we figure that out.
|
||||
# XXX: for the mega-lulz increasing width here increases draw
|
||||
# latency... so probably don't do it until we figure that out.
|
||||
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
|
||||
# a huge boon for avg interaction latency.
|
||||
|
@ -354,30 +356,6 @@ class BarItems(pg.GraphicsObject):
|
|||
if flip_cache:
|
||||
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):
|
||||
# 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.QtCore import (
|
||||
Qt,
|
||||
# QSize,
|
||||
QSize,
|
||||
QModelIndex,
|
||||
QItemSelectionModel,
|
||||
)
|
||||
|
@ -112,6 +112,7 @@ class CompleterView(QTreeView):
|
|||
|
||||
model = QStandardItemModel(self)
|
||||
self.labels = labels
|
||||
self._last_window_h: Optional[int] = None
|
||||
|
||||
# a std "tabular" config
|
||||
self.setItemDelegate(FontScaledDelegate(self))
|
||||
|
@ -126,6 +127,10 @@ class CompleterView(QTreeView):
|
|||
# self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
|
||||
|
||||
# ux settings
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
)
|
||||
self.setItemsExpandable(True)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
self.setAnimated(False)
|
||||
|
@ -153,23 +158,40 @@ class CompleterView(QTreeView):
|
|||
|
||||
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()
|
||||
cols = model.columnCount()
|
||||
|
||||
for i in range(cols):
|
||||
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())
|
||||
# print(f'font_h: {font_h}\n px_height: {px_height}')
|
||||
|
||||
# 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.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.update()
|
||||
|
||||
def is_selecting_d1(self) -> bool:
|
||||
cidx = self.selectionModel().currentIndex()
|
||||
|
@ -334,7 +356,7 @@ class CompleterView(QTreeView):
|
|||
else:
|
||||
model.setItem(idx.row(), 1, QStandardItem())
|
||||
|
||||
self.resize()
|
||||
self.resize_to_results()
|
||||
|
||||
return idx
|
||||
else:
|
||||
|
@ -404,7 +426,7 @@ class CompleterView(QTreeView):
|
|||
|
||||
def show_matches(self) -> None:
|
||||
self.show()
|
||||
self.resize()
|
||||
self.resize_to_results()
|
||||
|
||||
|
||||
class SearchBar(Edit):
|
||||
|
@ -457,7 +479,7 @@ class SearchWidget(QtWidgets.QWidget):
|
|||
# size it as we specify
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Fixed,
|
||||
QtWidgets.QSizePolicy.Fixed,
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
)
|
||||
|
||||
self.godwidget = godwidget
|
||||
|
|
|
@ -110,7 +110,7 @@ class DpiAwareFont:
|
|||
|
||||
mx_dpi = max(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
|
||||
inches = _font_sizes['lo'][self._font_size]
|
||||
|
@ -121,17 +121,29 @@ class DpiAwareFont:
|
|||
dpi = mn_dpi
|
||||
|
||||
# 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
|
||||
# relative aspect ratios or something?
|
||||
inches = inches * (1 / scale) * (1 + 6/16)
|
||||
inches = inches * (1 + 6/16)
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
)
|
||||
# apply the size
|
||||
|
@ -205,19 +217,26 @@ def hcolor(name: str) -> str:
|
|||
'svags': '#0a0e14',
|
||||
|
||||
# fifty shades
|
||||
'original': '#a9a9a9',
|
||||
'gray': '#808080', # like the kick
|
||||
'grayer': '#4c4c4c',
|
||||
'grayest': '#3f3f3f',
|
||||
'i3': '#494D4F',
|
||||
'jet': '#343434',
|
||||
'cadet': '#91A3B0',
|
||||
'marengo': '#91A3B0',
|
||||
'charcoal': '#36454F',
|
||||
'gunmetal': '#91A3B0',
|
||||
'battleship': '#848482',
|
||||
'davies': '#555555',
|
||||
|
||||
# bluish
|
||||
'charcoal': '#36454F',
|
||||
|
||||
# default bars
|
||||
'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
|
||||
'default_darkest': DarkPalette.COLOR_BACKGROUND_1,
|
||||
|
|
|
@ -151,8 +151,8 @@ class MainWindow(QtGui.QMainWindow):
|
|||
# XXX: for tiling wms this should scale
|
||||
# with the alloted window size.
|
||||
# TODO: detect for tiling and if untrue set some size?
|
||||
# size = (300, 500)
|
||||
size = (0, 0)
|
||||
size = (300, 500)
|
||||
#size = (0, 0)
|
||||
|
||||
title = 'piker chart (ur symbol is loading bby)'
|
||||
|
||||
|
@ -163,6 +163,7 @@ class MainWindow(QtGui.QMainWindow):
|
|||
|
||||
self._status_bar: QStatusBar = None
|
||||
self._status_label: QLabel = None
|
||||
self._size: Optional[tuple[int, int]] = None
|
||||
|
||||
@property
|
||||
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..."
|
||||
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
|
||||
_qt_win: QtGui.QMainWindow = None
|
||||
|
|
|
@ -22,6 +22,7 @@ from contextlib import asynccontextmanager
|
|||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
from pprint import pformat
|
||||
import platform
|
||||
import time
|
||||
from typing import Optional, Dict, Callable, Any
|
||||
import uuid
|
||||
|
@ -429,6 +430,7 @@ class OrderMode:
|
|||
|
||||
# TODO: make this not trash.
|
||||
# XXX: linux only for now
|
||||
if platform.system() != "Windows":
|
||||
result = await trio.run_process(
|
||||
[
|
||||
'notify-send',
|
||||
|
|
|
@ -25,6 +25,8 @@ import i3ipc
|
|||
i3 = i3ipc.Connection()
|
||||
t = i3.get_tree()
|
||||
|
||||
orig_win_id = t.find_focused().window
|
||||
|
||||
# for tws
|
||||
win_names: list[str] = [
|
||||
'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
|
||||
# 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..
|
||||
'click', '--window', win_id, '1',
|
||||
'click', '--window', win_id, '--repeat', '3', '1',
|
||||
|
||||
# hackzorzes
|
||||
'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