Compare commits

...

71 Commits

Author SHA1 Message Date
wattygetlood 98df996209 Set isn't serializable on std msgpack 2021-11-03 09:09:43 -04:00
wattygetlood 614089ae84 Only load 4 ib requests worth of bars on windows... 2021-11-03 09:09:43 -04:00
wattygetlood 3c9c772177 Hack search view on windows to 1/2 window height; needs a better solution 2021-11-03 09:09:43 -04:00
wattygetlood a5d38df689 Size the window to aproximately 1/3 the screen space 2021-11-03 09:09:43 -04:00
wattygetlood 02e9240986 No support for notifications (yet) on windows 2021-11-03 09:09:43 -04:00
wattygetlood ad623119fa Fix divide-by-zero when quote read is too fast in throttle task 2021-11-03 09:09:43 -04:00
wattygetlood 48ad97de2c Fix default `brokers.toml` copying since module move 2021-11-03 09:09:43 -04:00
wattygetlood 25eb6f0087 Configure window size based on screen dims on windows 2021-11-03 09:09:43 -04:00
Tyler Goodlet 6c86db8f2c Drop order status bar down a font px size 2021-11-03 09:09:02 -04:00
Tyler Goodlet 704cc80708 Only update curve lengths on non-negative index diffs 2021-11-03 09:09:02 -04:00
Tyler Goodlet 90f3db31bb Guard against empty array read in step update task 2021-11-03 09:09:02 -04:00
Tyler Goodlet 8f0662f3aa Factor out context cacher to `tractor.trionics` 2021-11-03 09:09:02 -04:00
Tyler Goodlet 43c59a2149 Error out clearing task on first quote being nan 2021-11-03 09:09:02 -04:00
Tyler Goodlet baf3fcb6fd Drop throttled rate margin to 100us 2021-11-03 09:09:02 -04:00
Tyler Goodlet be21df33c5 Turn on profiling for the moment 2021-11-03 09:09:02 -04:00
Tyler Goodlet 456cdc1820 De-densify some funcs 2021-11-03 09:09:02 -04:00
Tyler Goodlet d2d1f3223b Add some typing around web bs 2021-11-03 09:09:02 -04:00
Tyler Goodlet 0735d924a6 Rename feed bus entrypoint 2021-11-03 09:09:02 -04:00
Tyler Goodlet e978ae3151 Update some typing and add latency checks for binance 2021-11-03 09:09:02 -04:00
Tyler Goodlet 284e9397f7 Please please please let this dpi scaling hack work 2021-11-03 09:09:02 -04:00
Tyler Goodlet 905dc5a456 Port imports to tractor's new subpkg 2021-11-03 09:09:02 -04:00
Tyler Goodlet ef6eb3808e Repeat the click 3 times 2021-11-03 09:09:02 -04:00
Tyler Goodlet 3a3be89bd2 Start testing out trionics helpers, put vlm before rsi 2021-11-03 09:09:02 -04:00
Tyler Goodlet 4812cce8cf Make openGL flag actually work.. 2021-11-03 09:09:02 -04:00
wattygetlood 5bc2609a24 Only scale down for scale < 2 2021-11-03 09:09:02 -04:00
Tyler Goodlet baaf8d4a9d Revert to old shm "last" meaning last row 2021-11-03 09:09:02 -04:00
Tyler Goodlet 8b7e7fd110 Spawn and cache an fsp cluster ahead of time
Use a fixed worker count and don't respawn for every chart, instead
opting for a round-robin to tasks in a cluster and (for now) hoping for
the best in terms of trio scheduling, though we should obviously route
via symbol-locality next. This is currently a boon for chart spawning
startup times since actor creation is done AOT.

Additionally,
- use `zero_on_step` for dollar volume
- drop rsi on startup (again)
- add dollar volume (via fsp) along side unit volume
- litter more profiling to fsp chart startup sequence
- pre-define tick type classes for update loop
2021-11-03 09:09:02 -04:00
Tyler Goodlet ade0993d61 Start trionics mod with an `async_enter_all` 2021-11-03 09:09:02 -04:00
Tyler Goodlet 1ad7e9bae2 Activate/focus original window after feed reset 2021-11-03 09:09:02 -04:00
Tyler Goodlet ceb1152f2c Expose dollar volume to fsp engine
It can now be declared inside an fsp config dict under the name
`dolla_vlm`. We still need to offer an engine control that zeros
the newest sample value instead of copying from the previous.

This also litters the engine code with `pyqtgraph` profiling to see if
we can improve startup times - likely it'll mean pre-allocating a small
fsp daemon cluster at startup.
2021-11-03 09:09:02 -04:00
Tyler Goodlet 7d8202a63c Fix shm index update race
There was a lingering issue where the fsp daemon would sync its shm
array with the source data and we'd set the start/end indices to the
same value. Under some races a reader would then read an empty `.array`
which it wasn't expecting. This fixes that as well as tidies up the
`ShmArray.push()` logic and adds a temporary check in `.array` for zero
length if the array hasn't been written yet.

We can now start removing read array length checks in consumer code
and hopefully no more races will show up.
2021-11-03 09:09:02 -04:00
Tyler Goodlet 4783dd7efa TOSQUASH fix subplots.values() cuckup 2021-11-03 09:09:02 -04:00
Tyler Goodlet 4057209900 Add first draft of "dollar volume" fsp 2021-11-03 09:09:02 -04:00
Tyler Goodlet dde8697f71 Autoscale the y-range for all linked charts 2021-11-03 09:09:02 -04:00
Tyler Goodlet 926ee78216 `graphics_name` is more explicit then `name` 2021-11-03 09:09:02 -04:00
Tyler Goodlet 49227b6e57 Process framed ticks by type in main graphics loop
We are already packing framed ticks in extended lists from
the `.data._sampling.uniform_rate_send()` task so the natural solution
to avoid needless graphics cycles for HFT-ish feeds (like binance) is
to unpack those frames and for most cases only update graphics with the
"latest" data per loop iteration. Unpacking in this way also lessens
nested-iterations per tick type.

Btw, this also effectively solves all remaining issues of fast tick
feeds over-triggering the graphics loop renders as long as the original
quote stream is throttled appropriately, usually to the local display
rate.

Relates to #183, #192

Dirty deats:
- drop all per-tick rate checks, they were always somewhat pointless
  when iterating a frame of ticks per render cycle XD.
- unpack tick frame into ticks per frame type, and last of each type;
  the lasts are used to update each part of the UI/graphics by class.
- only skip the label update if we can't retrieve the last from from a
  graphics source array; it seems `chart.update_curve_from_array()`
  already does a `len` check internally.
- add some draft commented code for tick type classes and a possible
  wire framed tick data structure.
- move `chart_maxmin()` range computer to module level, bind a chart to
  it with a `partial.`
- only check rate limits in main quote loop thus reporting actual
  overages
- add in commented logic for only updating the "last" cleared price from
  the most recent framed value if we want to eventually (right now seems
  like this is only relevant to ib and it's dark trades: `utrade`).
- rename `_clear_throttle_rate` -> `_quote_throttle_rate`, drop
  `_book_throttle_rate`.
2021-11-03 09:09:02 -04:00
Tyler Goodlet 3f39c2bdfa Update fsps and overlays inside main OHLC chart update loop 2021-11-03 09:09:02 -04:00
Tyler Goodlet 6897393715 Fix color passthrough, make overlays a `dict` 2021-11-03 09:09:02 -04:00
Tyler Goodlet 549ff4ef11 Factor FSP subplot update code into func
This is in prep toward doing fsp graphics updates from the main quotes
update loop (where OHLC and volume are done). Updating fsp output from
that task should, for the majority of cases, be fine presuming the
processing is derived from the quote stream as a source. Further,
calling an update function on each fsp subplot/overlay is of course
faster then a full task switch - which is how it currently works with
a separate stream for every fsp output. This also will let us delay
adding full `Feed` support around fsp streams for the moment while still
getting quote throttling dictated by the quote stream.

Going forward, We can still support a separate task/fsp stream for
updates as needed (ex. some kind of fast external data source that isn't
synced with price data) but it should be enabled as needed required by
the user.
2021-11-03 09:09:02 -04:00
Tyler Goodlet 6751840568 More prep for FSP feeds
The major change is moving the fsp "daemon" (more like wanna-be fspd)
endpoint to use the newer `tractor.Portal.open_context()` and
bi-directional streaming api.

There's a few other things in here too:
- make a helper for allocating single colume fsp shm arrays
- rename some some fsp related functions to be more explicit on their
  purposes
2021-11-03 09:09:02 -04:00
Tyler Goodlet d0bad2e98e Make `.paint()` method always the last 2021-11-03 09:09:02 -04:00
Tyler Goodlet f269fe9732 Always draw a last step line with px width=2 2021-11-03 09:09:02 -04:00
Tyler Goodlet c37e6e2440 Clean up some imports, shift around some commented code 2021-11-03 09:09:02 -04:00
Tyler Goodlet 95baca219d Resize volume yaxis to in view range 2021-11-03 09:09:02 -04:00
Tyler Goodlet 6a82b1802b Update vlm sticky 2021-11-03 09:09:02 -04:00
Tyler Goodlet d751707e06 Pass curve color through to y sticky label 2021-11-03 09:09:02 -04:00
Tyler Goodlet 063b860cd4 Re-order grays by "lightness" 2021-11-03 09:09:02 -04:00
Tyler Goodlet 94793c276f Add back in rsi 2021-11-03 09:09:02 -04:00
Tyler Goodlet baaf6abec6 Increase current bar's pen size by a px 2021-11-03 09:09:02 -04:00
Tyler Goodlet a1b455349f Add dynamic subplot sizing logic, passthrouh step curve colors 2021-11-03 09:09:02 -04:00
Tyler Goodlet 940fc5d2f0 Use filled rect for current step
A `QRectF` is easier to make and draw (i think?) so use that and fill it
on volume events for decent sleek real-time look. Adjust the step array
generator to allow for an endpoints flag. Comment and/or clean out all
the old path filling calls that gave us perf issues..
2021-11-03 09:09:02 -04:00
Tyler Goodlet 6dd42bcfb3 Add test logic for range based volume curve filling 2021-11-03 09:09:02 -04:00
Tyler Goodlet 3670184c18 Bleh, try a bunch of stuff for step filling
Turns out the performance of updating and refilling step curves > 1k ish
points is super slow :sadkek:. Disabling the fill basically returns
normal performance, so it seems maybe we'll stick with unfilled volume
"bars" for now. The other tricky bit is getting the path to extend and
fill which is particularly slow if you use the `QPainterPath.united()`
(what `+` set op does) operation which seems to require an entire redraw
of the curve each paint iteration. Removing the pixel buffer cache makes
things that much worse too..

One technique i tried was only setting a `._fill` flag when so many
datums are in view (< 1k as determined by the chart widget), and this
helps, but under high load (trade rates) you still see more lag then
without the fill which makes me say screw it and let's stick with
unfilled bars for now. Trying go to get performant filled curves will be
an exercise for an aspiring graphics eng :P
2021-11-03 09:09:02 -04:00
Tyler Goodlet d6d284fec5 Add last step updates and path fill support 2021-11-03 09:09:02 -04:00
Tyler Goodlet c82046d59b Drop rsi from display by default 2021-11-03 09:09:02 -04:00
Tyler Goodlet 230fb00777 Add todo for new view padding testing 2021-11-03 09:09:02 -04:00
Tyler Goodlet 168c1eb2a9 Add volume plot as default
Toss in support for a "step mode" curve (unfinished atm) and use it to
plot from the `volume` field of the ohlcv shm array (if available).

changes to make it happen,
- dynamically generate the fsp sidepane form from an input config `dict`
  |_ dynamically generate the underlying `pydantic` model
  |_
- add a "volume checker" helper func that inspects the shm array
- toss in sidepane resize calls to avoid race where the ohlcv array
  is plotted too slowly compared to the volume and the chart somehow
  doesn't show..
- drop duplicate rsi2 cruft (previously used to test plots of the shm
  data)
2021-11-03 09:09:02 -04:00
Tyler Goodlet 699e670363 Invert 'c' (connection) array
In latest `pyqtgraph` it seems there's a discrepancy
since `function.arrayToQPath()` was reworked and now
we need to *not* connect the last point for each bar.
2021-11-03 09:09:02 -04:00
Tyler Goodlet 1e20b1d4dd Draft 'step' curve; couldn't get pg builtin to work 2021-11-03 09:09:02 -04:00
Tyler Goodlet df14ff052a Draft tina install section 2021-11-03 09:09:02 -04:00
Tyler Goodlet 8d34a2f13a Toss in references step mode impl 2021-11-03 09:09:02 -04:00
Tyler Goodlet 8d44706588 Add some type annots around pp msg handling 2021-11-03 08:58:44 -04:00
Tyler Goodlet cdf9f740f7 Expect accounts as tuple, don't start rt pnl on no live pp 2021-11-03 08:58:44 -04:00
Tyler Goodlet 96c8908e9c Comment on default account load order 2021-11-03 08:58:44 -04:00
Tyler Goodlet 3efe3b38b8 Avoid value error on puterizing unit name 2021-11-03 08:58:44 -04:00
Tyler Goodlet a6629520b1 Rage drop the limit size unit enum 2021-11-03 08:58:44 -04:00
Tyler Goodlet febb9310e8 Fix exit-slot-edge-case when only one discrete unit remains 2021-11-03 08:58:44 -04:00
Tyler Goodlet 8cfd046284 Keep slots ratio of 1 on derivs at startup 2021-11-03 08:58:44 -04:00
Tyler Goodlet 05a00c9dcc Force min pnl label width to avoid resizes on magnitude steps 2021-11-03 08:58:44 -04:00
Tyler Goodlet 12858e9113 Shorten edit name, passthrough kwargs to adder methods 2021-11-03 08:58:44 -04:00
Tyler Goodlet edcbc88a42 Update pp size label on settings changes
Resolves #232
2021-11-03 08:55:19 -04:00
31 changed files with 1477 additions and 754 deletions

View File

@ -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:

View File

@ -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):

View File

@ -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:

View File

@ -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__)
@ -213,7 +213,7 @@ class Client:
)
# repack in dict form
return {item[0]['symbol']: item[0]
for item in matches}
for item in matches}
async def bars(
self,
@ -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

View File

@ -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?!")

View File

@ -87,13 +87,21 @@ class Allocator(BaseModel):
symbol: Symbol
account: Optional[str] = 'paper'
size_unit: SizeUnit = 'currency'
# TODO: for enums this clearly doesn't fucking work, you can't set
# a default at startup by passing in a `dict` but yet you can set
# that value through assignment..for wtv cucked reason.. honestly, pure
# unintuitive garbage.
size_unit: str = 'currency'
_size_units: dict[str, Optional[str]] = _size_units
@validator('size_unit')
def lookup_key(cls, v):
@validator('size_unit', pre=True)
def maybe_lookup_key(cls, v):
# apply the corresponding enum key for the text "description" value
return v.name
if v not in _size_units:
return _size_units.inverse[v]
assert v in _size_units
return v
# TODO: if we ever want ot support non-uniform entry-slot-proportion
# "sizes"
@ -157,6 +165,9 @@ class Allocator(BaseModel):
slot_size = currency_per_slot / price
l_sub_pp = (self.currency_limit - live_cost_basis) / price
else:
raise ValueError(f"Not valid size unit '{size}'")
# an entry (adding-to or starting a pp)
if (
action == 'buy' and live_size > 0 or
@ -204,7 +215,14 @@ class Allocator(BaseModel):
# **without** going past a net-zero pp. if the pp is
# > 1.5x a slot size, then front load: exit a slot's and
# expect net-zero to be acquired on the final exit.
slot_size < pp_size < round((1.5*slot_size), ndigits=ld)
slot_size < pp_size < round((1.5*slot_size), ndigits=ld) or
# underlying requires discrete (int) units (eg. stocks)
# and thus our slot size (based on our limit) would
# exit a fractional unit's worth so, presuming we aren't
# supporting a fractional-units-style broker, we need
# exit the final unit.
ld == 0 and abs_live_size == 1
):
order_size = abs_live_size
@ -259,7 +277,7 @@ def mk_allocator(
# default allocation settings
defaults: dict[str, float] = {
'account': None, # select paper by default
'size_unit': _size_units['currency'],
'size_unit': 'currency', #_size_units['currency'],
'units_limit': 400,
'currency_limit': 5e3,
'slots': 4,
@ -274,8 +292,8 @@ def mk_allocator(
# load and retreive user settings for default allocations
# ``config.toml``
user_def = {
'currency_limit': 5e3,
'slots': 4,
'currency_limit': 6e3,
'slots': 6,
}
defaults.update(user_def)
@ -287,6 +305,7 @@ def mk_allocator(
asset_type = symbol.type_key
# specific configs by asset class / type
if asset_type in ('future', 'option', 'futures_option'):
@ -308,9 +327,12 @@ def mk_allocator(
alloc.currency_limit = round(startup_size, ndigits=2)
else:
startup_size = startup_pp.size
startup_size = abs(startup_pp.size)
if startup_size > alloc.units_limit:
alloc.units_limit = startup_size
if asset_type in ('future', 'option', 'futures_option'):
alloc.slots = alloc.units_limit
return alloc

View File

@ -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
@ -269,7 +279,7 @@ class TradesRelay:
positions: dict[str, dict[str, BrokerdPosition]]
# allowed account names
accounts: set[str]
accounts: tuple[str]
# count of connected ems clients for this ``brokerd``
consumers: int = 0
@ -414,6 +424,9 @@ async def open_brokerd_trades_dialogue(
)
try:
positions: list[BrokerdPosition]
accounts: tuple[str]
async with (
open_trades_endpoint as (brokerd_ctx, (positions, accounts,)),
brokerd_ctx.open_stream() as brokerd_trades_stream,
@ -449,7 +462,7 @@ async def open_brokerd_trades_dialogue(
relay = TradesRelay(
brokerd_dialogue=brokerd_trades_stream,
positions=pps,
accounts=set(accounts),
accounts=accounts,
consumers=1,
)
@ -1002,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
@ -1032,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

View File

@ -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,
)

View File

@ -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}')

View File

@ -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)

View File

@ -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``.

View File

@ -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.
"""

View File

@ -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,

View File

@ -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:

View File

@ -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

80
piker/trionics.py 100644
View File

@ -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()

View File

@ -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)

View File

@ -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,67 +916,78 @@ 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)
curve.update_from_array(
x=array['index'],
y=array[data_key],
**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],
**kwargs
)
return curve
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..

View File

@ -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,21 +180,47 @@ class FastAppendCurve(pg.PlotCurveItem):
# # self.path.moveTo(new_x[0], new_y[0])
# self.path.connectPath(old_path)
if append_length:
# print(f"append_length: {append_length}")
new_x = x[-append_length - 2:-1]
new_y = y[-append_length - 2:-1]
# print((new_x, new_y))
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]
# print((new_x, new_y))
append_path = pg.functions.arrayToQPath(
new_x,
new_y,
connect='all'
connect='all',
# finiteCheck=False,
)
# 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 = 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)
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

View File

@ -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()

View File

@ -48,7 +48,7 @@ from ._style import hcolor, _font, _font_small, DpiAwareFont
from ._label import FormatLabel
class FontAndChartAwareLineEdit(QLineEdit):
class Edit(QLineEdit):
def __init__(
@ -369,13 +369,14 @@ class FieldsForm(QWidget):
key: str,
label_name: str,
value: str,
readonly: bool = False,
) -> FontAndChartAwareLineEdit:
) -> Edit:
# TODO: maybe a distint layout per "field" item?
label = self.add_field_label(label_name)
edit = FontAndChartAwareLineEdit(
edit = Edit(
parent=self,
# width_in_chars=6,
)
@ -386,6 +387,7 @@ class FieldsForm(QWidget):
}}
"""
)
edit.setReadOnly(readonly)
edit.setText(str(value))
self.form.addRow(label, edit)
@ -478,13 +480,15 @@ def mk_form(
for key, conf in fields_schema.items():
wtype = conf['type']
label = str(conf.get('label', key))
kwargs = conf.get('kwargs', {})
# plain (line) edit field
if wtype == 'edit':
w = form.add_edit_field(
key,
label,
conf['default_value']
conf['default_value'],
**kwargs,
)
# drop-down selection
@ -493,7 +497,8 @@ def mk_form(
w = form.add_select_field(
key,
label,
values
values,
**kwargs,
)
w._key = key
@ -648,11 +653,21 @@ def mk_fill_status_bar(
font_size=bar_label_font_size,
font_color='gunmetal',
)
# size according to dpi scaled fonted contents to avoid
# resizes on magnitude changes (eg. 9 -> 10 %)
min_w = _font.boundingRect('1000.0M% pnl').width()
left_label.setMinimumWidth(min_w)
left_label.resize(
min_w,
left_label.size().height(),
)
bar_labels_lhs.addSpacing(5/8 * bar_h)
bar_labels_lhs.addWidget(
left_label,
alignment=Qt.AlignLeft | Qt.AlignTop,
# XXX: doesn't seem to actually push up against
# the status bar?
alignment=Qt.AlignRight | Qt.AlignTop,
)
# this hbox is added as a layout by the paner maker/caller
@ -717,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?

View File

@ -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))

View File

@ -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')

View File

@ -36,7 +36,7 @@ from PyQt5.QtWidgets import (
from ._forms import (
# FontScaledDelegate,
FontAndChartAwareLineEdit,
Edit,
)
@ -97,7 +97,7 @@ class Selection(Field[DataType], Generic[DataType]):
class Edit(Field[DataType], Generic[DataType]):
'''An edit field which takes a number.
'''
widget_factory = FontAndChartAwareLineEdit
widget_factory = Edit
class AllocatorPane(BaseModel):

View File

@ -54,6 +54,7 @@ async def update_pnl_from_feed(
feed: Feed,
order_mode: OrderMode, # noqa
tracker: PositionTracker,
) -> None:
'''Real-time display the current pp's PnL in the appropriate label.
@ -76,7 +77,8 @@ async def update_pnl_from_feed(
types = ('bid', 'last', 'last', 'utrade')
else:
raise RuntimeError('No pp?!?!')
log.info(f'No position (yet) for {tracker.alloc.account}@{key}')
return
# real-time update pnl on the status pane
try:
@ -152,7 +154,7 @@ class SettingsPane:
'''Called on any order pane drop down selection change.
'''
log.info(f'selection input: {text}')
log.info(f'selection input {key}:{text}')
self.on_ui_settings_change(key, text)
def on_ui_settings_change(
@ -209,30 +211,31 @@ class SettingsPane:
# WRITE any settings to current pp's allocator
try:
value = puterize(value)
if key == 'limit':
if size_unit == 'currency':
alloc.currency_limit = value
else:
alloc.units_limit = value
elif key == 'slots':
alloc.slots = int(value)
elif key == 'size_unit':
# TODO: if there's a limit size unit change re-compute
# the current settings in the new units
if key == 'size_unit':
# implicit re-write of value if input
# is the "text name" of the units.
# yah yah, i know this is badd..
alloc.size_unit = value
else:
raise ValueError(f'Unknown setting {key}')
value = puterize(value)
if key == 'limit':
if size_unit == 'currency':
alloc.currency_limit = value
else:
alloc.units_limit = value
elif key == 'slots':
alloc.slots = int(value)
else:
raise ValueError(f'Unknown setting {key}')
log.info(f'settings change: {key}: {value}')
except ValueError:
log.error(f'Invalid value for `{key}`: {value}')
# READ out settings and update UI
# READ out settings and update the status UI / settings widgets
suffix = {'currency': ' $', 'units': ' u'}[size_unit]
limit = alloc.limit()
@ -259,6 +262,9 @@ class SettingsPane:
self.form.fields['slots'].setText(str(alloc.slots))
self.form.fields['limit'].setText(str(limit))
# update of level marker size label based on any new settings
tracker.update_from_pp()
# TODO: maybe return a diff of settings so if we can an error we
# can have general input handling code to report it through the
# UI in some way?
@ -339,6 +345,7 @@ class SettingsPane:
update_pnl_from_feed,
feed,
mode,
tracker,
)
# immediately display in status label

View File

@ -49,7 +49,7 @@ from PyQt5 import QtCore
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt,
# QSize,
QSize,
QModelIndex,
QItemSelectionModel,
)
@ -72,7 +72,7 @@ from ._style import (
_font,
hcolor,
)
from ._forms import FontAndChartAwareLineEdit, FontScaledDelegate
from ._forms import Edit, FontScaledDelegate
log = get_logger(__name__)
@ -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)
@ -152,24 +157,41 @@ class CompleterView(QTreeView):
self._font_size = size
self.setStyleSheet(f"font: {size}px")
#def resizeEvent(self, event: 'QEvent') -> None:
# self.resize_to_results()
# super().resizeEvent(event)
def resize(self):
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
self.setMinimumSize(self.width(), rows * row_px)
self.setMaximumSize(self.width() + 10, rows * row_px)
# 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)
#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,10 +426,10 @@ class CompleterView(QTreeView):
def show_matches(self) -> None:
self.show()
self.resize()
self.resize_to_results()
class SearchBar(FontAndChartAwareLineEdit):
class SearchBar(Edit):
mode_name: str = 'search'
@ -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

View File

@ -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:
# TODO: this denominator should probably be determined from
# relative aspect ratios or something?
inches = inches * (1 / scale) * (1 + 6/16)
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 + 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,

View File

@ -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,7 +163,8 @@ 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

View File

@ -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
@ -47,7 +48,7 @@ from ._position import (
)
from ._label import FormatLabel
from ._window import MultiStatus
from ..clearing._messages import Order
from ..clearing._messages import Order, BrokerdPosition
from ._forms import open_form_input_handling
@ -429,16 +430,17 @@ class OrderMode:
# TODO: make this not trash.
# XXX: linux only for now
result = await trio.run_process(
[
'notify-send',
'-u', 'normal',
'-t', '10000',
'piker',
f'alert: {msg}',
],
)
log.runtime(result)
if platform.system() != "Windows":
result = await trio.run_process(
[
'notify-send',
'-u', 'normal',
'-t', '10000',
'piker',
f'alert: {msg}',
],
)
log.runtime(result)
def on_cancel(
self,
@ -529,7 +531,12 @@ async def open_order_mode(
book: OrderBook
trades_stream: tractor.MsgStream
position_msgs: dict
# The keys in this dict **must** be in set our set of "normalized"
# symbol names (i.e. the same names you'd get back in search
# results) in order for position msgs to correctly trigger the
# display of a position indicator on screen.
position_msgs: dict[str, list[BrokerdPosition]]
# spawn EMS actor-service
async with (
@ -563,7 +570,9 @@ async def open_order_mode(
providers=symbol.brokers
)
# use only loaded accounts according to brokerd
# XXX: ``brokerd`` delivers a set of account names that it allows
# use of but the user also can define the accounts they'd like
# to use, in order, in their `brokers.toml` file.
accounts = {}
for name in brokerd_accounts:
# ensure name is in ``brokers.toml``
@ -571,7 +580,10 @@ async def open_order_mode(
# first account listed is the one we select at startup
# (aka order based selection).
pp_account = next(iter(accounts.keys())) if accounts else 'paper'
pp_account = next(
# choose first account based on line order from `brokers.toml`.
iter(accounts.keys())
) if accounts else 'paper'
# NOTE: requires the backend exactly specifies
# the expected symbol key in its positions msg.
@ -617,8 +629,8 @@ async def open_order_mode(
# alloc?
pp_tracker.update_from_pp()
# on existing position, show pp tracking graphics
if pp_tracker.startup_pp.size != 0:
# if no position, don't show pp tracking graphics
pp_tracker.show()
pp_tracker.hide_info()
@ -802,12 +814,13 @@ async def process_trades_and_update_ui(
tracker = mode.trackers[msg['account']]
tracker.live_pp.update_from_msg(msg)
tracker.update_from_pp()
# update order pane widgets
tracker.update_from_pp()
mode.pane.update_status_ui(tracker)
# display pnl
mode.pane.display_pnl(tracker)
if tracker.live_pp.size:
# display pnl
mode.pane.display_pnl(tracker)
# short circuit to next msg to avoid
# unnecessary msg content lookups

View File

@ -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',
])