Merge pull request #1 from selimaj-dev/i-dont-like-python
I dont like python
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "pulse-sdk-py"
|
||||||
|
version = "0.0.1-alpha.0"
|
||||||
|
description = "Python SDK for PulseWire Protocol (PWP), a trading engine"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
authors = [
|
||||||
|
{name = "Klesti Selimaj"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import sys
|
||||||
|
from typing import Generic, TypeVar, Type
|
||||||
|
|
||||||
|
from . import general
|
||||||
|
from . import plugin
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=general.PulseWire)
|
||||||
|
TT = TypeVar("TT", bound=general.PulseWire)
|
||||||
|
|
||||||
|
class PulseWire(Generic[T, TT]):
|
||||||
|
def __init__(self, wire_cls: Type[T]):
|
||||||
|
self.wire_cls = wire_cls
|
||||||
|
|
||||||
|
def on(self, req: TT):
|
||||||
|
...
|
||||||
|
|
||||||
|
def send_raw(self, data: bytes):
|
||||||
|
sys.stdout.buffer.write(len(data).to_bytes(8, "little"))
|
||||||
|
sys.stdout.buffer.write(data)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
while True:
|
||||||
|
len_buf = sys.stdin.buffer.read(8)
|
||||||
|
|
||||||
|
# Standard EOF check: zero bytes returned from read means stdin closed
|
||||||
|
if not len_buf:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(len_buf) < 8:
|
||||||
|
raise EOFError("Unexpected EOF while reading message length header")
|
||||||
|
|
||||||
|
length = int.from_bytes(len_buf, "little")
|
||||||
|
if length == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
buffer = sys.stdin.buffer.read(length)
|
||||||
|
if len(buffer) != length:
|
||||||
|
raise EOFError("Unexpected EOF while reading payload")
|
||||||
|
|
||||||
|
self.onRaw(buffer)
|
||||||
|
|
||||||
|
def onRaw(self, data: bytes):
|
||||||
|
# Call from_com on the actual class passed in __init__
|
||||||
|
result: tuple[TT, int] = self.wire_cls.from_com(data=data)
|
||||||
|
req, _ = result
|
||||||
|
|
||||||
|
self.on(req)
|
||||||
|
|
||||||
|
# Dynamic dispatch based on class name (e.g., onMyRequest)
|
||||||
|
handler = getattr(self, f"on{req.__class__.__name__}", None)
|
||||||
|
if handler and callable(handler):
|
||||||
|
handler(req)
|
||||||
|
|
||||||
|
class Strategy(PulseWire[plugin.StrategyEngineMessage, plugin.StrategyEngineMessageType]):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(plugin.StrategyEngineMessage)
|
||||||
|
|
||||||
|
def onInitialize(self, msg: plugin.StrategyEngineMessage.Initialize):
|
||||||
|
...
|
||||||
|
|
||||||
|
def onWatchList(self, msg: plugin.StrategyEngineMessage.WatchList):
|
||||||
|
...
|
||||||
|
|
||||||
|
def onCommand(self, msg: plugin.StrategyEngineMessage.Command):
|
||||||
|
...
|
||||||
|
|
||||||
|
def onCandleUpdate(self, msg: plugin.StrategyEngineMessage.CandleUpdate):
|
||||||
|
...
|
||||||
|
|
||||||
|
def onCandlestick(self, msg: plugin.StrategyEngineMessage.Candlestick):
|
||||||
|
...
|
||||||
|
|
||||||
|
def send(self, req: plugin.StrategyMessageType):
|
||||||
|
self.send_raw(req.to_com())
|
||||||
|
|
||||||
|
def log(self, log: general.EventLog):
|
||||||
|
msg = plugin.StrategyMessage.Log()
|
||||||
|
msg.log = log
|
||||||
|
self.send(msg)
|
||||||
|
|
||||||
|
def getWatchList(self):
|
||||||
|
self.send(plugin.StrategyMessage.GetWatchList())
|
||||||
|
|
||||||
|
def getCandleStick(self, symbol: str, interval: general.CandleIntervalType, count: general.u32):
|
||||||
|
msg = plugin.StrategyMessage.RequestCandlestick()
|
||||||
|
msg.symbol = symbol
|
||||||
|
msg.interval = interval
|
||||||
|
msg.count = count
|
||||||
|
self.send(msg)
|
||||||
|
|
||||||
|
def unsubscribeAll(self):
|
||||||
|
self.send(plugin.StrategyMessage.UnsubscribeAll())
|
||||||
|
|
||||||
|
def signal(self, signal: plugin.StrategySignal):
|
||||||
|
msg = plugin.StrategyMessage.Signal()
|
||||||
|
msg.signal = signal
|
||||||
|
self.send(msg)
|
||||||
|
|
||||||
|
class Risk(PulseWire[plugin.RiskEngineMessage, plugin.RiskEngineMessageType]):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(plugin.RiskEngineMessage)
|
||||||
|
|
||||||
|
def onInitialize(self, msg: plugin.RiskEngineMessage.Initialize):
|
||||||
|
...
|
||||||
|
|
||||||
|
def onWatchList(self, msg: plugin.RiskEngineMessage.WatchList):
|
||||||
|
...
|
||||||
|
|
||||||
|
def onCommand(self, msg: plugin.RiskEngineMessage.Command):
|
||||||
|
...
|
||||||
|
|
||||||
|
def onSignal(self, msg: plugin.RiskEngineMessage.Signal):
|
||||||
|
...
|
||||||
|
|
||||||
|
def send(self, req: plugin.RiskMessageType):
|
||||||
|
self.send_raw(req.to_com())
|
||||||
|
|
||||||
|
def log(self, log: general.EventLog):
|
||||||
|
msg = plugin.RiskMessage.Log()
|
||||||
|
msg.log = log
|
||||||
|
self.send(msg)
|
||||||
|
|
||||||
|
def getWatchList(self):
|
||||||
|
self.send(plugin.RiskMessage.GetWatchList())
|
||||||
|
|
||||||
|
def approveSignal(self, signal: general.Signal):
|
||||||
|
msg = plugin.RiskMessage.Approve()
|
||||||
|
msg.signal = signal
|
||||||
|
self.send(msg)
|
||||||
|
|
||||||
|
def rejectSignal(self, reason: str):
|
||||||
|
msg = plugin.RiskMessage.Reject()
|
||||||
|
msg.reason = reason
|
||||||
|
self.send(msg)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
import struct
|
||||||
|
import typing
|
||||||
|
|
||||||
|
T = typing.TypeVar("T")
|
||||||
|
class PulseUnit(typing.Generic[T]):
|
||||||
|
value: T
|
||||||
|
|
||||||
|
def __init__(self, value: T):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __class_getitem__(cls, item):
|
||||||
|
new_cls = type(
|
||||||
|
f"{cls.__name__}[{item.__name__}]",
|
||||||
|
(cls,),
|
||||||
|
{"_type": item}
|
||||||
|
)
|
||||||
|
return new_cls
|
||||||
|
|
||||||
|
def to_com(self) -> bytes: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_com(cls, data: bytes, offset: int = 0) -> tuple[typing.Any, int]: ...
|
||||||
|
|
||||||
|
B = typing.TypeVar("B", bound=type[PulseUnit])
|
||||||
|
def pwp_number(fmt, value_type):
|
||||||
|
size = struct.calcsize(fmt)
|
||||||
|
|
||||||
|
def decorator(cls: B) -> B:
|
||||||
|
def to_com(self):
|
||||||
|
return struct.pack(fmt, self.value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_com(cls, data, offset=0):
|
||||||
|
value = struct.unpack_from(fmt, data, offset)[0]
|
||||||
|
return cls(value), offset + size
|
||||||
|
|
||||||
|
cls.to_com = to_com
|
||||||
|
cls.from_com = from_com
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
# Unsigned integers
|
||||||
|
@pwp_number("<B", int)
|
||||||
|
class u8(PulseUnit): pass
|
||||||
|
|
||||||
|
@pwp_number("<H", int)
|
||||||
|
class u16(PulseUnit): pass
|
||||||
|
|
||||||
|
@pwp_number("<I", int)
|
||||||
|
class u32(PulseUnit): pass
|
||||||
|
|
||||||
|
@pwp_number("<Q", int)
|
||||||
|
class u64(PulseUnit): pass
|
||||||
|
|
||||||
|
# Signed integers
|
||||||
|
@pwp_number("<b", int)
|
||||||
|
class i8(PulseUnit): pass
|
||||||
|
|
||||||
|
@pwp_number("<h", int)
|
||||||
|
class i16(PulseUnit): pass
|
||||||
|
|
||||||
|
@pwp_number("<i", int)
|
||||||
|
class i32(PulseUnit): pass
|
||||||
|
|
||||||
|
@pwp_number("<q", int)
|
||||||
|
class i64(PulseUnit): pass
|
||||||
|
|
||||||
|
# Floating point
|
||||||
|
@pwp_number("<f", float)
|
||||||
|
class f32(PulseUnit): pass
|
||||||
|
|
||||||
|
@pwp_number("<d", float)
|
||||||
|
class f64(PulseUnit): pass
|
||||||
|
|
||||||
|
class decimal(PulseUnit):
|
||||||
|
# Exactly 16 bytes to match Rust's [u8; 16] output
|
||||||
|
size = 16
|
||||||
|
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = Decimal(value)
|
||||||
|
|
||||||
|
def to_com(self) -> bytes:
|
||||||
|
sign, digits, exponent = self.value.as_tuple()
|
||||||
|
|
||||||
|
if not isinstance(exponent, int):
|
||||||
|
raise ValueError("Cannot serialize NaN or Infinity")
|
||||||
|
|
||||||
|
scale = -exponent
|
||||||
|
mantissa = int("".join(map(str, digits)))
|
||||||
|
|
||||||
|
# Handle positive exponents (e.g. 5E+2 -> scale 0, mantissa 500)
|
||||||
|
if scale < 0:
|
||||||
|
mantissa = mantissa * (10 ** -scale)
|
||||||
|
scale = 0
|
||||||
|
|
||||||
|
# Max scale allowed in rust_decimal is 28
|
||||||
|
if scale > 28:
|
||||||
|
raise ValueError(f"Scale {scale} exceeds maximum allowed (28)")
|
||||||
|
|
||||||
|
# Split 96-bit mantissa into three 32-bit chunks
|
||||||
|
lo = mantissa & 0xFFFFFFFF
|
||||||
|
mid = (mantissa >> 32) & 0xFFFFFFFF
|
||||||
|
hi = (mantissa >> 64) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
# Construct flags: scale is in bits 16-23, sign is bit 31
|
||||||
|
flags = (scale << 16) & 0x00FF0000
|
||||||
|
if sign:
|
||||||
|
flags |= 0x80000000
|
||||||
|
|
||||||
|
return struct.pack("<4I", flags, lo, mid, hi)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_com(cls, data: bytes, offset: int = 0) -> tuple[typing.Any, int]:
|
||||||
|
# Unpack four 32-bit unsigned integers (Little-Endian)
|
||||||
|
flags, lo, mid, hi = struct.unpack_from("<4I", data, offset)
|
||||||
|
|
||||||
|
# Extract scale (Bits 16-23)
|
||||||
|
scale = (flags >> 16) & 0xFF
|
||||||
|
|
||||||
|
# Extract sign (Bit 31)
|
||||||
|
is_negative = (flags & 0x80000000) != 0
|
||||||
|
|
||||||
|
# Reconstruct the 96-bit integer mantissa
|
||||||
|
mantissa = lo | (mid << 32) | (hi << 64)
|
||||||
|
|
||||||
|
# Convert to Python Decimal
|
||||||
|
value = Decimal(mantissa) / (Decimal(10) ** scale)
|
||||||
|
if is_negative:
|
||||||
|
value = -value
|
||||||
|
|
||||||
|
return cls(value), offset + cls.size
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
from typing import TypeAlias
|
||||||
|
|
||||||
|
from .pwp import pwp, pwp_enum, PulseWire
|
||||||
|
from .adv_types import *
|
||||||
|
from . import units
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class MarketTrend(PulseWire):
|
||||||
|
class Bullish: pass
|
||||||
|
class Bearish: pass
|
||||||
|
class Neutral: pass
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class LogKind(PulseWire):
|
||||||
|
class Info: pass
|
||||||
|
class Warn: pass
|
||||||
|
class Err: pass
|
||||||
|
class Debug: pass
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class CandleInterval(PulseWire):
|
||||||
|
class OneMinute(PulseWire): pass
|
||||||
|
class ThreeMinutes(PulseWire): pass
|
||||||
|
class FiveMinutes(PulseWire): pass
|
||||||
|
class FifteenMinutes(PulseWire): pass
|
||||||
|
class ThirtyMinutes(PulseWire): pass
|
||||||
|
class OneHour(PulseWire): pass
|
||||||
|
class TwoHours(PulseWire): pass
|
||||||
|
class FourHours(PulseWire): pass
|
||||||
|
class EightHours(PulseWire): pass
|
||||||
|
class TwelveHours(PulseWire): pass
|
||||||
|
class OneDay(PulseWire): pass
|
||||||
|
class ThreeDays(PulseWire): pass
|
||||||
|
class OneWeek(PulseWire): pass
|
||||||
|
class OneMonth(PulseWire): pass
|
||||||
|
|
||||||
|
@pwp
|
||||||
|
class Signal(PulseWire):
|
||||||
|
symbol: str
|
||||||
|
kind: units.Direction
|
||||||
|
confidence: f32
|
||||||
|
size: f64
|
||||||
|
price: units.USD
|
||||||
|
take_profit: units.USD
|
||||||
|
stop_loss: units.USD
|
||||||
|
|
||||||
|
MarketTrendType: TypeAlias = (
|
||||||
|
MarketTrend.Bullish
|
||||||
|
| MarketTrend.Bearish
|
||||||
|
| MarketTrend.Neutral
|
||||||
|
)
|
||||||
|
|
||||||
|
LogKindType: TypeAlias = (
|
||||||
|
LogKind.Info
|
||||||
|
| LogKind.Warn
|
||||||
|
| LogKind.Err
|
||||||
|
| LogKind.Debug
|
||||||
|
)
|
||||||
|
|
||||||
|
CandleIntervalType: TypeAlias = (
|
||||||
|
CandleInterval.OneMinute
|
||||||
|
| CandleInterval.ThreeMinutes
|
||||||
|
| CandleInterval.FiveMinutes
|
||||||
|
| CandleInterval.FifteenMinutes
|
||||||
|
| CandleInterval.ThirtyMinutes
|
||||||
|
| CandleInterval.OneHour
|
||||||
|
| CandleInterval.TwoHours
|
||||||
|
| CandleInterval.FourHours
|
||||||
|
| CandleInterval.EightHours
|
||||||
|
| CandleInterval.TwelveHours
|
||||||
|
| CandleInterval.OneDay
|
||||||
|
| CandleInterval.ThreeDays
|
||||||
|
| CandleInterval.OneWeek
|
||||||
|
| CandleInterval.OneMonth
|
||||||
|
)
|
||||||
|
|
||||||
|
@pwp
|
||||||
|
class EventLog(PulseWire):
|
||||||
|
kind: LogKindType
|
||||||
|
name: str
|
||||||
|
message: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def info(cls, name: str, message: str):
|
||||||
|
log = cls()
|
||||||
|
log.kind = LogKind.Info()
|
||||||
|
log.name = name
|
||||||
|
log.message = message
|
||||||
|
return log
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def warn(cls, name: str, message: str):
|
||||||
|
log = cls()
|
||||||
|
log.kind = LogKind.Warn()
|
||||||
|
log.name = name
|
||||||
|
log.message = message
|
||||||
|
return log
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def error(cls, name: str, message: str):
|
||||||
|
log = cls()
|
||||||
|
log.kind = LogKind.Err()
|
||||||
|
log.name = name
|
||||||
|
log.message = message
|
||||||
|
return log
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def debug(cls, name: str, message: str):
|
||||||
|
log = cls()
|
||||||
|
log.kind = LogKind.Debug()
|
||||||
|
log.name = name
|
||||||
|
log.message = message
|
||||||
|
return log
|
||||||
|
@pwp
|
||||||
|
class Position(PulseWire):
|
||||||
|
symbol: units.Symbol
|
||||||
|
size: f64
|
||||||
|
entry_price: units.USD
|
||||||
|
profit: units.USD
|
||||||
|
|
||||||
|
@pwp
|
||||||
|
class MarketItem(PulseWire):
|
||||||
|
symbol: units.Symbol
|
||||||
|
price: units.USD
|
||||||
|
trend: f64
|
||||||
|
volume_24h: units.USD
|
||||||
|
|
||||||
|
@pwp
|
||||||
|
class Candle(PulseWire):
|
||||||
|
open_time: u64
|
||||||
|
close_time: u64
|
||||||
|
coin: str
|
||||||
|
interval: str
|
||||||
|
open: decimal
|
||||||
|
high: decimal
|
||||||
|
low: decimal
|
||||||
|
close: decimal
|
||||||
|
volume: decimal
|
||||||
|
num_trades: u64
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
from typing import Optional, TypeAlias
|
||||||
|
|
||||||
|
from .pwp import pwp, pwp_enum, PulseWire
|
||||||
|
from .adv_types import *
|
||||||
|
from . import units, general
|
||||||
|
|
||||||
|
@pwp
|
||||||
|
class StrategySignal(PulseWire):
|
||||||
|
symbol: str
|
||||||
|
side: units.Direction
|
||||||
|
confidence: f32
|
||||||
|
price: Optional[f64]
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class StrategyMessage(PulseWire):
|
||||||
|
class Log(PulseWire):
|
||||||
|
log: general.EventLog
|
||||||
|
|
||||||
|
class GetWatchList(PulseWire): pass
|
||||||
|
|
||||||
|
class RequestCandlestick(PulseWire):
|
||||||
|
symbol: str
|
||||||
|
interval: general.CandleIntervalType
|
||||||
|
count: u32
|
||||||
|
|
||||||
|
class Subscribe(PulseWire):
|
||||||
|
subscription: u8
|
||||||
|
|
||||||
|
class Unsubscribe(PulseWire):
|
||||||
|
subscription: u8
|
||||||
|
|
||||||
|
class UnsubscribeAll(PulseWire): pass
|
||||||
|
|
||||||
|
class Signal(PulseWire):
|
||||||
|
signal: StrategySignal
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class RiskMessage(PulseWire):
|
||||||
|
class Log(PulseWire):
|
||||||
|
log: general.EventLog
|
||||||
|
|
||||||
|
class GetWatchList(PulseWire): pass
|
||||||
|
|
||||||
|
class Approve(PulseWire):
|
||||||
|
signal: general.Signal
|
||||||
|
|
||||||
|
class Reject(PulseWire):
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class StrategyEngineMessage(PulseWire):
|
||||||
|
class Initialize(PulseWire): pass
|
||||||
|
|
||||||
|
class WatchList(PulseWire):
|
||||||
|
watchlist: list[general.MarketItem]
|
||||||
|
|
||||||
|
class Command(PulseWire):
|
||||||
|
command: str
|
||||||
|
args: list[str]
|
||||||
|
|
||||||
|
class CandleUpdate(PulseWire):
|
||||||
|
symbol: str
|
||||||
|
interval: general.CandleInterval
|
||||||
|
candle: general.Candle
|
||||||
|
|
||||||
|
class Candlestick(PulseWire):
|
||||||
|
symbol: str
|
||||||
|
interval: general.CandleInterval
|
||||||
|
candles: list[general.Candle]
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class RiskEngineMessage(PulseWire):
|
||||||
|
class Initialize(PulseWire): pass
|
||||||
|
|
||||||
|
class WatchList(PulseWire):
|
||||||
|
watchlist: list[general.MarketItem]
|
||||||
|
|
||||||
|
class Command(PulseWire):
|
||||||
|
command: str
|
||||||
|
args: list[str]
|
||||||
|
|
||||||
|
class Signal(PulseWire):
|
||||||
|
signal: general.Signal
|
||||||
|
|
||||||
|
|
||||||
|
StrategyMessageType: TypeAlias = (
|
||||||
|
StrategyMessage.Log
|
||||||
|
| StrategyMessage.GetWatchList
|
||||||
|
| StrategyMessage.RequestCandlestick
|
||||||
|
| StrategyMessage.Subscribe
|
||||||
|
| StrategyMessage.Unsubscribe
|
||||||
|
| StrategyMessage.UnsubscribeAll
|
||||||
|
| StrategyMessage.Signal
|
||||||
|
)
|
||||||
|
|
||||||
|
RiskMessageType: TypeAlias = (
|
||||||
|
RiskMessage.Log
|
||||||
|
| RiskMessage.GetWatchList
|
||||||
|
| RiskMessage.Approve
|
||||||
|
| RiskMessage.Reject
|
||||||
|
)
|
||||||
|
|
||||||
|
StrategyEngineMessageType: TypeAlias = (
|
||||||
|
StrategyEngineMessage.Initialize
|
||||||
|
| StrategyEngineMessage.WatchList
|
||||||
|
| StrategyEngineMessage.Command
|
||||||
|
| StrategyEngineMessage.CandleUpdate
|
||||||
|
| StrategyEngineMessage.Candlestick
|
||||||
|
)
|
||||||
|
|
||||||
|
RiskEngineMessageType: TypeAlias = (
|
||||||
|
RiskEngineMessage.Initialize
|
||||||
|
| RiskEngineMessage.WatchList
|
||||||
|
| RiskEngineMessage.Command
|
||||||
|
| RiskEngineMessage.Signal
|
||||||
|
)
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import struct
|
||||||
|
import typing
|
||||||
|
import types
|
||||||
|
from typing import (
|
||||||
|
Protocol,
|
||||||
|
TypeVar,
|
||||||
|
Any,
|
||||||
|
get_type_hints,
|
||||||
|
get_origin,
|
||||||
|
get_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .adv_types import *
|
||||||
|
|
||||||
|
|
||||||
|
class PulseWire(Protocol):
|
||||||
|
def to_com(self) -> bytes:
|
||||||
|
return bytes()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_com(cls, data: bytes, offset: int = 0) -> tuple[typing.Any, int]:
|
||||||
|
return (None, offset)
|
||||||
|
|
||||||
|
class PWPError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_size(data: bytes, offset: int, size: int):
|
||||||
|
if offset + size > len(data):
|
||||||
|
raise PWPError(
|
||||||
|
f"Buffer underflow: need {size} bytes at offset {offset}, "
|
||||||
|
f"only {len(data) - offset} available"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_value(value, typ) -> bytes:
|
||||||
|
origin = get_origin(typ)
|
||||||
|
|
||||||
|
# Optional[T]
|
||||||
|
if origin in (typing.Union, types.UnionType):
|
||||||
|
args = get_args(typ)
|
||||||
|
if type(None) in args:
|
||||||
|
if value is None:
|
||||||
|
return b"\x00"
|
||||||
|
real = next(x for x in args if x is not type(None))
|
||||||
|
return b"\x01" + encode_value(value, real)
|
||||||
|
|
||||||
|
# list[T] or list
|
||||||
|
if origin is list or typ is list:
|
||||||
|
args = get_args(typ)
|
||||||
|
item_type = args[0] if args else Any
|
||||||
|
|
||||||
|
out = len(value).to_bytes(4, "little")
|
||||||
|
for item in value:
|
||||||
|
# If item_type is Any, infer type dynamically from the item itself
|
||||||
|
actual_type = item_type if item_type is not Any else type(item)
|
||||||
|
out += encode_value(item, actual_type)
|
||||||
|
return out
|
||||||
|
|
||||||
|
# tuple[T, ...]
|
||||||
|
if origin is tuple or typ is tuple:
|
||||||
|
args = get_args(typ)
|
||||||
|
out = len(value).to_bytes(4, "little")
|
||||||
|
for item, item_type in zip(value, args if args else [type(x) for x in value]):
|
||||||
|
out += encode_value(item, item_type)
|
||||||
|
return out
|
||||||
|
|
||||||
|
# dict[K, V]
|
||||||
|
if origin is dict or typ is dict:
|
||||||
|
args = get_args(typ)
|
||||||
|
key_type, value_type = args if args else (Any, Any)
|
||||||
|
out = len(value).to_bytes(4, "little")
|
||||||
|
for k, v in value.items():
|
||||||
|
out += encode_value(k, key_type if key_type is not Any else type(k))
|
||||||
|
out += encode_value(v, value_type if value_type is not Any else type(v))
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Primitives
|
||||||
|
if typ is bool:
|
||||||
|
return b"\x01" if value else b"\x00"
|
||||||
|
if typ is int:
|
||||||
|
return struct.pack("<q", value)
|
||||||
|
if typ is float:
|
||||||
|
return struct.pack("<d", value)
|
||||||
|
if typ is str:
|
||||||
|
raw = value.encode()
|
||||||
|
return len(raw).to_bytes(4, "little") + raw
|
||||||
|
if typ is bytes:
|
||||||
|
return len(value).to_bytes(4, "little") + value
|
||||||
|
|
||||||
|
# Custom PWP
|
||||||
|
if hasattr(value, "to_com"):
|
||||||
|
return value.to_com()
|
||||||
|
|
||||||
|
raise PWPError(f"Cannot encode type {typ}")
|
||||||
|
|
||||||
|
|
||||||
|
def decode_value(data: bytes, offset: int, typ):
|
||||||
|
origin = get_origin(typ)
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
if origin in (typing.Union, types.UnionType):
|
||||||
|
args = get_args(typ)
|
||||||
|
if type(None) in args:
|
||||||
|
ensure_size(data, offset, 1)
|
||||||
|
present = data[offset]
|
||||||
|
offset += 1
|
||||||
|
if present == 0:
|
||||||
|
return None, offset
|
||||||
|
real = next(x for x in args if x is not type(None))
|
||||||
|
return decode_value(data, offset, real)
|
||||||
|
|
||||||
|
# list
|
||||||
|
if origin is list or typ is list:
|
||||||
|
args = get_args(typ)
|
||||||
|
item_type = args[0] if args else Any
|
||||||
|
|
||||||
|
ensure_size(data, offset, 4)
|
||||||
|
count = int.from_bytes(data[offset:offset+4], "little")
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for _ in range(count):
|
||||||
|
item, offset = decode_value(data, offset, item_type)
|
||||||
|
result.append(item)
|
||||||
|
|
||||||
|
return result, offset
|
||||||
|
|
||||||
|
# Primitives & Custom fallbacks...
|
||||||
|
if typ is bool:
|
||||||
|
ensure_size(data, offset, 1)
|
||||||
|
return data[offset] != 0, offset + 1
|
||||||
|
if typ is int:
|
||||||
|
ensure_size(data, offset, 8)
|
||||||
|
return struct.unpack_from("<q", data, offset)[0], offset + 8
|
||||||
|
if typ is float:
|
||||||
|
ensure_size(data, offset, 8)
|
||||||
|
return struct.unpack_from("<d", data, offset)[0], offset + 8
|
||||||
|
if typ is str:
|
||||||
|
ensure_size(data, offset, 4)
|
||||||
|
length = int.from_bytes(data[offset:offset+4], "little")
|
||||||
|
offset += 4
|
||||||
|
ensure_size(data, offset, length)
|
||||||
|
return data[offset:offset+length].decode(), offset + length
|
||||||
|
if typ is bytes:
|
||||||
|
ensure_size(data, offset, 4)
|
||||||
|
length = int.from_bytes(data[offset:offset+4], "little")
|
||||||
|
offset += 4
|
||||||
|
ensure_size(data, offset, length)
|
||||||
|
return data[offset:offset+length], offset + length
|
||||||
|
|
||||||
|
if hasattr(typ, "from_com"):
|
||||||
|
return typ.from_com(data, offset)
|
||||||
|
|
||||||
|
raise PWPError(f"Cannot decode type {typ}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar(
|
||||||
|
"T",
|
||||||
|
bound=type[PulseWire]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pwp(cls: T) -> T:
|
||||||
|
|
||||||
|
fields = get_type_hints(cls)
|
||||||
|
|
||||||
|
|
||||||
|
def to_com(self):
|
||||||
|
|
||||||
|
out = b""
|
||||||
|
|
||||||
|
for name, typ in fields.items():
|
||||||
|
|
||||||
|
if not hasattr(self, name):
|
||||||
|
raise PWPError(
|
||||||
|
f"Missing field {name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
out += encode_value(
|
||||||
|
getattr(self, name),
|
||||||
|
typ
|
||||||
|
)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_com(
|
||||||
|
cls,
|
||||||
|
data,
|
||||||
|
offset=0
|
||||||
|
):
|
||||||
|
|
||||||
|
obj = cls.__new__(cls)
|
||||||
|
|
||||||
|
for name, typ in fields.items():
|
||||||
|
|
||||||
|
value, offset = decode_value(
|
||||||
|
data,
|
||||||
|
offset,
|
||||||
|
typ
|
||||||
|
)
|
||||||
|
|
||||||
|
setattr(
|
||||||
|
obj,
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
|
||||||
|
return obj, offset
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cls.to_com = to_com
|
||||||
|
cls.from_com = from_com
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def pwp_enum(cls: T) -> T:
|
||||||
|
|
||||||
|
variants = {}
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
|
||||||
|
for name, variant in list(cls.__dict__.items()):
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(variant, type)
|
||||||
|
and variant.__module__ == cls.__module__
|
||||||
|
):
|
||||||
|
|
||||||
|
variant.__annotations__ = {
|
||||||
|
"_id": u8,
|
||||||
|
**getattr(
|
||||||
|
variant,
|
||||||
|
"__annotations__",
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
variant._id = u8(index)
|
||||||
|
|
||||||
|
variant = pwp(variant)
|
||||||
|
|
||||||
|
variants[index] = variant
|
||||||
|
|
||||||
|
setattr(
|
||||||
|
cls,
|
||||||
|
name,
|
||||||
|
variant
|
||||||
|
)
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def to_com(self):
|
||||||
|
|
||||||
|
for idx, variant in variants.items():
|
||||||
|
|
||||||
|
if isinstance(self, variant):
|
||||||
|
|
||||||
|
return (
|
||||||
|
idx.to_bytes(1, "little")
|
||||||
|
+ self.to_com()
|
||||||
|
)
|
||||||
|
|
||||||
|
raise PWPError(
|
||||||
|
f"Unknown enum variant {type(self)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_com(
|
||||||
|
enum_cls,
|
||||||
|
data,
|
||||||
|
offset=0
|
||||||
|
):
|
||||||
|
|
||||||
|
ensure_size(
|
||||||
|
data,
|
||||||
|
offset,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
enum_id = data[offset]
|
||||||
|
# offset += 1
|
||||||
|
|
||||||
|
|
||||||
|
if enum_id not in variants:
|
||||||
|
raise PWPError(
|
||||||
|
f"Unknown enum id {enum_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return variants[enum_id].from_com(
|
||||||
|
data,
|
||||||
|
offset
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
cls.to_com = to_com
|
||||||
|
cls.from_com = from_com
|
||||||
|
|
||||||
|
return cls
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from .pwp import pwp, pwp_enum, PulseWire
|
||||||
|
from .adv_types import *
|
||||||
|
|
||||||
|
@pwp
|
||||||
|
class Symbol(PulseWire):
|
||||||
|
unit: str
|
||||||
|
|
||||||
|
@pwp
|
||||||
|
class USD(PulseWire):
|
||||||
|
unit: f64
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class Direction(PulseWire):
|
||||||
|
class Buy: pass
|
||||||
|
class Sell: pass
|
||||||
|
|
||||||
|
@pwp_enum
|
||||||
|
class Volatility(PulseWire):
|
||||||
|
class Low: pass
|
||||||
|
class Medium: pass
|
||||||
|
class High: pass
|
||||||
Reference in New Issue
Block a user