Compare commits
3 Commits
08d159e652
...
b30372f4fe
| Author | SHA1 | Date |
|---|---|---|
|
|
b30372f4fe | |
|
|
60b4526fad | |
|
|
c04cc0e87f |
|
|
@ -20,10 +20,14 @@ Handy cross-broker utils.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
# from functools import partial
|
||||
from typing import (
|
||||
Type,
|
||||
)
|
||||
|
||||
import json
|
||||
import httpx
|
||||
import logging
|
||||
from msgspec import Struct
|
||||
|
||||
from piker.log import (
|
||||
colorize_json,
|
||||
|
|
@ -97,6 +101,12 @@ class DataThrottle(BrokerError):
|
|||
'''
|
||||
# TODO: add in throttle metrics/feedback
|
||||
|
||||
class SchemaMismatch(BrokerError):
|
||||
'''
|
||||
Market `Pair` fields mismatch, likely due to provider API update.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def resproc(
|
||||
resp: httpx.Response,
|
||||
|
|
@ -123,3 +133,45 @@ def resproc(
|
|||
log.debug(f"Received json contents:\n{colorize_json(msg)}")
|
||||
|
||||
return msg if return_json else resp
|
||||
|
||||
|
||||
def get_or_raise_on_pair_schema_mismatch(
|
||||
pair_type: Type[Struct],
|
||||
fields_data: dict,
|
||||
provider_name: str,
|
||||
api_url: str|None = None,
|
||||
) -> Struct:
|
||||
'''
|
||||
Boilerplate helper around assset-`Pair` field schema mismatches,
|
||||
normally due to provider API updates.
|
||||
|
||||
'''
|
||||
try:
|
||||
pair: Struct = pair_type(**fields_data)
|
||||
return pair
|
||||
except TypeError as err:
|
||||
|
||||
from tractor.devx.pformat import ppfmt
|
||||
repr_data: str = ppfmt(fields_data)
|
||||
report: str = (
|
||||
f'Field mismatch we need to codify!\n'
|
||||
f'\n'
|
||||
f'{pair_type!r}({repr_data})'
|
||||
f'\n'
|
||||
f'^^^ {err.args[0]!r} ^^^\n'
|
||||
f'\n'
|
||||
f"Don't panic, prolly {provider_name!r} "
|
||||
f"changed their symbology schema..\n"
|
||||
)
|
||||
if (
|
||||
api_url
|
||||
or
|
||||
(api_url := pair_type._api_url)
|
||||
):
|
||||
report += (
|
||||
f'\n'
|
||||
f'Check out their API docs here:\n'
|
||||
f'{api_url}\n'
|
||||
)
|
||||
|
||||
raise SchemaMismatch(report) from err
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ from piker import config
|
|||
from piker.clearing._messages import (
|
||||
Order,
|
||||
)
|
||||
from piker.brokers._util import (
|
||||
get_or_raise_on_pair_schema_mismatch,
|
||||
)
|
||||
from piker.accounting import (
|
||||
Asset,
|
||||
digits_to_dec,
|
||||
|
|
@ -370,20 +373,12 @@ class Client:
|
|||
item['filters'] = filters
|
||||
|
||||
pair_type: Type = PAIRTYPES[venue]
|
||||
try:
|
||||
pair: Pair = pair_type(**item)
|
||||
except Exception as e:
|
||||
e.add_note(
|
||||
f'\n'
|
||||
f'New or removed field we need to codify!\n'
|
||||
f'pair-type: {pair_type!r}\n'
|
||||
f'\n'
|
||||
f"Don't panic, prolly stupid binance changed their symbology schema again..\n"
|
||||
f'Check out their API docs here:\n'
|
||||
f'\n'
|
||||
f'https://binance-docs.github.io/apidocs/spot/en/#exchange-information\n'
|
||||
pair: Pair = get_or_raise_on_pair_schema_mismatch(
|
||||
pair_type=pair_type,
|
||||
fields_data=item,
|
||||
provider_name='binance',
|
||||
api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information',
|
||||
)
|
||||
raise
|
||||
pair_table[pair.symbol.upper()] = pair
|
||||
|
||||
# update an additional top-level-cross-venue-table
|
||||
|
|
@ -581,8 +576,8 @@ class Client:
|
|||
self,
|
||||
mkt: MktPair,
|
||||
|
||||
start_dt: datetime | None = None,
|
||||
end_dt: datetime | None = None,
|
||||
start_dt: datetime|None = None,
|
||||
end_dt: datetime|None = None,
|
||||
|
||||
as_np: bool = True,
|
||||
|
||||
|
|
@ -609,7 +604,11 @@ class Client:
|
|||
start_time = binance_timestamp(start_dt)
|
||||
end_time = binance_timestamp(end_dt)
|
||||
|
||||
bs_pair: Pair = self._pairs[mkt.bs_fqme.upper()]
|
||||
import tractor
|
||||
with tractor.devx.maybe_open_crash_handler():
|
||||
bs_pair: Pair = self._pairs[
|
||||
mkt.bs_fqme.upper()
|
||||
]
|
||||
|
||||
# https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data
|
||||
bars = await self._api(
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import tractor
|
|||
from piker.brokers import (
|
||||
open_cached_client,
|
||||
NoData,
|
||||
SymbolNotFound,
|
||||
)
|
||||
from piker._cacheables import (
|
||||
async_lifo_cache,
|
||||
|
|
@ -305,6 +306,10 @@ async def get_mkt_info(
|
|||
|
||||
# uppercase since kraken bs_mktid is always upper
|
||||
if 'binance' not in fqme.lower():
|
||||
log.warning(
|
||||
f'Missing `.<provider>` part in fqme ??\n'
|
||||
f'fqme: {fqme!r}\n'
|
||||
)
|
||||
fqme += '.binance'
|
||||
|
||||
mkt_mode: str = ''
|
||||
|
|
@ -319,6 +324,12 @@ async def get_mkt_info(
|
|||
venue: str = venue.upper()
|
||||
venue_lower: str = venue.lower()
|
||||
|
||||
if not venue:
|
||||
raise SymbolNotFound(
|
||||
f'Invalid or missing .<venue> part in fqme?\n'
|
||||
f'fqme: {fqme!r}\n'
|
||||
)
|
||||
|
||||
# XXX TODO: we should change the usdtm_futes name to just
|
||||
# usdm_futes (dropping the tether part) since it turns out that
|
||||
# there are indeed USD-tokens OTHER THEN tether being used as
|
||||
|
|
@ -360,6 +371,8 @@ async def get_mkt_info(
|
|||
if not mkt_mode:
|
||||
mkt_mode: str = f'{venue_lower}_futes'
|
||||
|
||||
await tractor.pause()
|
||||
|
||||
async with open_cached_client(
|
||||
'binance',
|
||||
) as client:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ Per market data-type definitions and schemas types.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
ClassVar,
|
||||
Literal,
|
||||
)
|
||||
from decimal import Decimal
|
||||
|
|
@ -203,6 +204,8 @@ class FutesPair(Pair):
|
|||
# NOTE: see `.data._symcache.SymbologyCache.load()` for why
|
||||
ns_path: str = 'piker.brokers.binance:FutesPair'
|
||||
|
||||
_api_url: ClassVar[str] = 'https://binance-docs.github.io/apidocs/spot/en/#exchange-information'
|
||||
|
||||
# NOTE: for compat with spot pairs and `MktPair.src: Asset`
|
||||
# processing..
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ from .symbols import (
|
|||
)
|
||||
from ...log import get_logger
|
||||
from .venues import (
|
||||
is_expired,
|
||||
is_venue_open,
|
||||
sesh_times,
|
||||
is_venue_closure,
|
||||
|
|
@ -496,7 +497,7 @@ class Client:
|
|||
await self.ib.reqContractDetailsAsync(contract)
|
||||
)[0]
|
||||
# convert to makt-native tz
|
||||
tz: str = details.timeZoneId
|
||||
tz: str = details.timeZoneId or 'EST'
|
||||
end_dt = end_dt.in_tz(tz)
|
||||
first_dt: DateTime = from_timestamp(first).in_tz(tz)
|
||||
last_dt: DateTime = from_timestamp(last).in_tz(tz)
|
||||
|
|
@ -508,10 +509,18 @@ class Client:
|
|||
_open_now: bool = is_venue_open(
|
||||
con_deats=details,
|
||||
)
|
||||
_is_expired: bool = is_expired(
|
||||
con_deats=details,
|
||||
)
|
||||
|
||||
# XXX, do gap detections.
|
||||
has_closure_gap: bool = False
|
||||
if (
|
||||
# XXX, expired tracts can't be introspected
|
||||
# for open/closure intervals due to ib's chitty
|
||||
# details seemingly..
|
||||
not _is_expired
|
||||
and
|
||||
last_dt.add(seconds=sample_period_s)
|
||||
<
|
||||
end_dt
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from typing import (
|
|||
|
||||
import exchange_calendars as xcals
|
||||
from pendulum import (
|
||||
parse,
|
||||
now,
|
||||
Duration,
|
||||
Interval,
|
||||
|
|
@ -56,6 +57,17 @@ if TYPE_CHECKING:
|
|||
)
|
||||
|
||||
|
||||
def is_expired(
|
||||
con_deats: ContractDetails,
|
||||
) -> bool:
|
||||
'''
|
||||
Predicate
|
||||
|
||||
'''
|
||||
expiry_dt: datetime = parse(con_deats.realExpirationDate)
|
||||
return expiry_dt.date() >= now().date()
|
||||
|
||||
|
||||
def has_weekend(
|
||||
period: Interval,
|
||||
) -> bool:
|
||||
|
|
@ -170,7 +182,22 @@ def sesh_times(
|
|||
get the (day-agnostic) times for the start/end.
|
||||
|
||||
'''
|
||||
earliest_sesh: Interval = next(iter_sessions(con_deats))
|
||||
# ?TODO, lookup the next front contract instead?
|
||||
if is_expired(con_deats):
|
||||
raise ValueError(
|
||||
f'Contract is already expired!\n'
|
||||
f'Choose an active alt contract instead.\n'
|
||||
f'con_deats: {con_deats!r}\n'
|
||||
)
|
||||
|
||||
maybe_sessions: list[Interval] = list(iter_sessions(con_deats))
|
||||
if not maybe_sessions:
|
||||
raise ValueError(
|
||||
f'Contract has no trading-session info?\n'
|
||||
f'con_deats: {con_deats!r}\n'
|
||||
)
|
||||
|
||||
earliest_sesh: Interval = maybe_sessions[0]
|
||||
return (
|
||||
earliest_sesh.start.time(),
|
||||
earliest_sesh.end.time(),
|
||||
|
|
@ -211,7 +238,13 @@ def is_venue_closure(
|
|||
'''
|
||||
open: Time
|
||||
close: Time
|
||||
open, close = sesh_times(con_deats)
|
||||
maybe_oc: tuple|None = sesh_times(con_deats)
|
||||
if maybe_oc is None:
|
||||
# XXX, should never get here.
|
||||
breakpoint()
|
||||
return False
|
||||
|
||||
open, close = maybe_oc
|
||||
|
||||
# ensure times are in mkt-native timezone
|
||||
tz: str = con_deats.timeZoneId
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ from piker.brokers._util import (
|
|||
SymbolNotFound,
|
||||
BrokerError,
|
||||
DataThrottle,
|
||||
get_or_raise_on_pair_schema_mismatch,
|
||||
)
|
||||
from piker.accounting import Transaction
|
||||
from piker.log import get_logger
|
||||
|
|
@ -502,7 +503,16 @@ class Client:
|
|||
|
||||
# NOTE: always cache in pairs tables for faster lookup
|
||||
with tractor.devx.maybe_open_crash_handler(): # as bxerr:
|
||||
pair = Pair(xname=xkey, **data)
|
||||
# pair = Pair(xname=xkey, **data)
|
||||
pair: Pair = get_or_raise_on_pair_schema_mismatch(
|
||||
pair_type=Pair,
|
||||
fields_data=dict(
|
||||
xname=xkey,
|
||||
**data,
|
||||
),
|
||||
provider_name='kraken',
|
||||
# api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information',
|
||||
)
|
||||
|
||||
# register the above `Pair` structs for all
|
||||
# key-sets/monikers: a set of 4 (frickin) tables
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ Symbology defs and search.
|
|||
|
||||
'''
|
||||
from decimal import Decimal
|
||||
from typing import (
|
||||
ClassVar,
|
||||
)
|
||||
|
||||
import tractor
|
||||
|
||||
|
|
@ -86,9 +89,14 @@ class Pair(Struct):
|
|||
short_position_limit: float = 0
|
||||
long_position_limit: float = float('inf')
|
||||
|
||||
# TODO, add API note when this was added!
|
||||
# execution_venue: str|None = None
|
||||
|
||||
# TODO: should we make this a literal NamespacePath ref?
|
||||
ns_path: str = 'piker.brokers.kraken:Pair'
|
||||
|
||||
_api_url: ClassVar[str] = 'https://docs.kraken.com/api/docs/rest-api/get-tradable-asset-pairs'
|
||||
|
||||
@property
|
||||
def bs_mktid(self) -> str:
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -206,8 +206,8 @@ pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
|||
# xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
|
||||
|
||||
# XXX since, we're like, always hacking new shite all-the-time. Bp
|
||||
tractor = { git = "https://github.com/goodboy/tractor.git", branch ="main" }
|
||||
# tractor = { git = "https://github.com/goodboy/tractor.git", branch ="main" }
|
||||
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
||||
# ------ goodboy ------
|
||||
# hackin dev-envs, usually there's something new he's hackin in..
|
||||
# tractor = { path = "../tractor", editable = true }
|
||||
tractor = { path = "../tractor", editable = true }
|
||||
|
|
|
|||
46
uv.lock
46
uv.lock
|
|
@ -1034,7 +1034,7 @@ requires-dist = [
|
|||
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
||||
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
|
||||
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
|
||||
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=main" },
|
||||
{ name = "tractor", editable = "../tractor" },
|
||||
{ name = "trio", specifier = ">=0.27" },
|
||||
{ name = "trio-typing", specifier = ">=0.10.0" },
|
||||
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
|
||||
|
|
@ -1680,7 +1680,7 @@ wheels = [
|
|||
[[package]]
|
||||
name = "tractor"
|
||||
version = "0.1.0a6.dev0"
|
||||
source = { git = "https://github.com/goodboy/tractor.git?branch=main#e77198bb64f0467a50e251ed140daee439752354" }
|
||||
source = { editable = "../tractor" }
|
||||
dependencies = [
|
||||
{ name = "bidict" },
|
||||
{ name = "cffi" },
|
||||
|
|
@ -1693,6 +1693,48 @@ dependencies = [
|
|||
{ name = "wrapt" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "bidict", specifier = ">=0.23.1" },
|
||||
{ name = "cffi", specifier = ">=1.17.1" },
|
||||
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
||||
{ name = "platformdirs", specifier = ">=4.4.0" },
|
||||
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
||||
{ name = "trio", specifier = ">0.27" },
|
||||
{ name = "wrapt", specifier = ">=1.16.0,<2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||
{ name = "xonsh", specifier = ">=0.22.2" },
|
||||
]
|
||||
devx = [
|
||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
||||
repl = [
|
||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||
{ name = "xonsh", specifier = ">=0.22.2" },
|
||||
]
|
||||
testing = [
|
||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tricycle"
|
||||
version = "0.4.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue