From aa513b884f47b8adbf3bffefe4eb7bf53619a9a2 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 04:08:48 +0200 Subject: [PATCH 01/20] Base PulseWire --- __init__.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..1ec6b30 --- /dev/null +++ b/__init__.py @@ -0,0 +1,33 @@ +import sys +import struct + +class PulseWire: + def __init__(self): + ... + + def on_raw_engine_request(self, data: bytes): + ... + + 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(struct.calcsize("P")) + + if len(len_buf) == 0: + return None + + length = int.from_bytes(len_buf, "little") + + if length == 0: + return None + + buffer = sys.stdin.buffer.read(length) + + if len(buffer) != length: + raise EOFError("Unexpected EOF while reading payload") + + self.on_raw_engine_request(buffer) \ No newline at end of file From 70157b8bd919da6352a084596d06ff7054faa225 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 04:22:46 +0200 Subject: [PATCH 02/20] PWP --- __init__.py => src/__init__.py | 0 src/pwp.py | 179 +++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) rename __init__.py => src/__init__.py (100%) create mode 100644 src/pwp.py diff --git a/__init__.py b/src/__init__.py similarity index 100% rename from __init__.py rename to src/__init__.py diff --git a/src/pwp.py b/src/pwp.py new file mode 100644 index 0000000..eb5f4da --- /dev/null +++ b/src/pwp.py @@ -0,0 +1,179 @@ +import struct +import typing +from typing import get_type_hints, get_origin, get_args + + +class PWPError(Exception): + pass + + +def encode_value(value, typ) -> bytes: + origin = get_origin(typ) + + # Optional[T] + if origin is typing.Union: + args = get_args(typ) + + if type(None) in args: + if value is None: + return b"\x00" + + real_type = next(t for t in args if t is not type(None)) + return b"\x01" + encode_value(value, real_type) + + # list[T] + if origin is list: + item_type = get_args(typ)[0] + + out = len(value).to_bytes(4, "little") + + for item in value: + out += encode_value(item, item_type) + + return out + + # dict[K, V] + if origin is dict: + key_type, value_type = get_args(typ) + + out = len(value).to_bytes(4, "little") + + for k, v in value.items(): + out += encode_value(k, key_type) + out += encode_value(v, value_type) + + return out + + # primitives + if typ is str: + data = value.encode("utf-8") + return len(data).to_bytes(4, "little") + data + + if typ is bytes: + return len(value).to_bytes(4, "little") + value + + if typ is int: + return struct.pack(" Date: Tue, 28 Jul 2026 05:02:15 +0200 Subject: [PATCH 03/20] working enum --- src/pwp.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/pwp.py b/src/pwp.py index eb5f4da..3305468 100644 --- a/src/pwp.py +++ b/src/pwp.py @@ -2,10 +2,11 @@ import struct import typing from typing import get_type_hints, get_origin, get_args - class PWPError(Exception): pass +class uint8: + pass def encode_value(value, typ) -> bytes: origin = get_origin(typ) @@ -61,7 +62,9 @@ def encode_value(value, typ) -> bytes: if typ is bool: return b"\x01" if value else b"\x00" - # nested PWP class + if typ is uint8: + return struct.pack(" Date: Tue, 28 Jul 2026 05:09:04 +0200 Subject: [PATCH 04/20] working enum --- src/pwp.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/pwp.py b/src/pwp.py index 3305468..e889321 100644 --- a/src/pwp.py +++ b/src/pwp.py @@ -169,15 +169,17 @@ def pwp(cls): return output @classmethod - def from_com(data): + def from_com(cls, data): values = {} offset = 0 + obj = cls.__new__(cls) + for name, typ in fields.items(): value, offset = decode_value(data, offset, typ) - values[name] = value + setattr(obj, name, value) - return cls(**values) + return obj cls.to_com = to_com cls.from_com = from_com @@ -185,6 +187,8 @@ def pwp(cls): return cls def pwp_enum(cls): + variants = {} + for index, (name, value) in enumerate(cls.__dict__.items()): if isinstance(value, type) and value.__module__ == cls.__module__: value.__annotations__ = { @@ -196,6 +200,34 @@ def pwp_enum(cls): value = pwp(value) + variants[index] = value setattr(cls, name, value) + @classmethod + def from_com(enum_cls, data): + enum_id = data[0] + + if enum_id not in variants: + raise PWPError(f"Unknown enum id: {enum_id}") + + variant = variants[enum_id] + + # Decode the rest of the data + obj = variant.__new__(variant) + + offset = 1 # skip enum id byte + + fields = get_type_hints(variant) + + for name, typ in fields.items(): + if name == "_id": + continue + + value, offset = decode_value(data, offset, typ) + setattr(obj, name, value) + + return obj + + cls.from_com = from_com + return cls \ No newline at end of file From 5f2ff55d35260aaf8a1bb57a3181fff3cecb221d Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 05:12:43 +0200 Subject: [PATCH 05/20] Units and general --- src/general.py | 25 +++++++++++++++++++++++++ src/units.py | 20 ++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/general.py create mode 100644 src/units.py diff --git a/src/general.py b/src/general.py new file mode 100644 index 0000000..3e9fbfd --- /dev/null +++ b/src/general.py @@ -0,0 +1,25 @@ +from pwp import pwp, pwp_enum +import units + +@pwp_enum +class MarketTrend: + class Bullish: pass + class Bearish: pass + class Neutral: pass + +@pwp_enum +class LogKind: + class Info: pass + class Warn: pass + class Err: pass + class Debug: pass + +@pwp_enum +class Signal: + symbol: str + kind: units.Direction + confidence: float + size: float + price: units.USD + take_profit: units.USD + stop_loss: units.USD diff --git a/src/units.py b/src/units.py new file mode 100644 index 0000000..dc8fea8 --- /dev/null +++ b/src/units.py @@ -0,0 +1,20 @@ +from pwp import pwp, pwp_enum + +@pwp +class Symbol: + unit: str + +@pwp +class USD: + unit: float + +@pwp_enum +class Direction: + class Buy: pass + class Sell: pass + +@pwp_enum +class Volatility: + class Low: pass + class Medium: pass + class High: pass From e884b7982f120dbe640b694c3973f659b174c696 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 05:43:29 +0200 Subject: [PATCH 06/20] Advanced types --- src/adv_types.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ src/general.py | 5 +++-- src/pwp.py | 27 ++++++++++------------- src/units.py | 3 ++- 4 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 src/adv_types.py diff --git a/src/adv_types.py b/src/adv_types.py new file mode 100644 index 0000000..c729109 --- /dev/null +++ b/src/adv_types.py @@ -0,0 +1,57 @@ +import struct + +def pwp_number(fmt): + size = struct.calcsize(fmt) + + def decorator(cls): + def __init__(self, value): + self.value = value + + 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.__init__ = __init__ + cls.to_com = to_com + cls.from_com = from_com + + return cls + + return decorator + +# Unsigned integers +@pwp_number(" bytes: origin = get_origin(typ) @@ -23,7 +21,7 @@ def encode_value(value, typ) -> bytes: return b"\x01" + encode_value(value, real_type) # list[T] - if origin is list: + elif origin is list: item_type = get_args(typ)[0] out = len(value).to_bytes(4, "little") @@ -62,10 +60,7 @@ def encode_value(value, typ) -> bytes: if typ is bool: return b"\x01" if value else b"\x00" - if typ is uint8: - return struct.pack(" Date: Tue, 28 Jul 2026 06:07:12 +0200 Subject: [PATCH 07/20] General --- src/general.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/general.py b/src/general.py index 49af437..e73d3a9 100644 --- a/src/general.py +++ b/src/general.py @@ -15,7 +15,7 @@ class LogKind: class Err: pass class Debug: pass -@pwp_enum +@pwp class Signal: symbol: str kind: units.Direction @@ -24,3 +24,15 @@ class Signal: price: units.USD take_profit: units.USD stop_loss: units.USD + +@pwp +class EventLog: + kind: LogKind + name: str + message: str + +class Position: + symbol: units.Symbol + size: f64 + entry_price: units.USD + profit: units.USD \ No newline at end of file From ab4826dccaa18a34added0bc1ca35b325720c683 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 06:28:51 +0200 Subject: [PATCH 08/20] Plugin types --- src/general.py | 40 +++++++++++++++++++++++- src/plugin.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/plugin.py diff --git a/src/general.py b/src/general.py index e73d3a9..f981133 100644 --- a/src/general.py +++ b/src/general.py @@ -15,6 +15,23 @@ class LogKind: class Err: pass class Debug: pass +@pwp_enum +class CandleInterval: + class OneMinute: pass + class ThreeMinutes: pass + class FiveMinutes: pass + class FifteenMinutes: pass + class ThirtyMinutes: pass + class OneHour: pass + class TwoHours: pass + class FourHours: pass + class EightHours: pass + class TwelveHours: pass + class OneDay: pass + class ThreeDays: pass + class OneWeek: pass + class OneMonth: pass + @pwp class Signal: symbol: str @@ -31,8 +48,29 @@ class EventLog: name: str message: str +@pwp class Position: symbol: units.Symbol size: f64 entry_price: units.USD - profit: units.USD \ No newline at end of file + profit: units.USD + +@pwp +class MarketItem: + symbol: units.Symbol + price: units.USD + trend: f64 + volume_24h: units.USD + +@pwp +class Candle: + open_time: u64 + close_time: u64 + coin: str + interval: str + open: f64 + high: f64 + low: f64 + close: f64 + volume: f64 + num_trades: u64 diff --git a/src/plugin.py b/src/plugin.py new file mode 100644 index 0000000..dcd3166 --- /dev/null +++ b/src/plugin.py @@ -0,0 +1,83 @@ +from pwp import pwp, pwp_enum +from adv_types import * +import units +import general +from typing import Optional + +@pwp +class StrategySignal: + symbol: str + side: units.Direction + confidence: f32 + price: Optional[f64] + +@pwp_enum +class StrategyMessage: + class Log: + log: general.EventLog + + class GetWatchList: pass + + class RequestCandlestick: + symbol: str + interval: u8 + count: u32 + + class Subscribe: + subscription: u8 + + class Unsubscribe: + subscription: u8 + + class UnsubscribeAll: pass + + class Signal: + signal: StrategySignal + +@pwp_enum +class RiskMessage: + class Log: + log: general.EventLog + + class GetWatchList: pass + + class Approve: + signal: general.Signal + + class Reject: + reason: str + +@pwp_enum +class StrategyEngineMessage: + class Initialize: pass + + class WatchList: + watchlist: list[general.MarketItem] + + class Command: + command: str + args: list[str] + + class CandleUpdate: + symbol: str + interval: general.CandleInterval + candle: general.Candle + + class CandleStick: + symbol: str + interval: general.CandleInterval + candles: list[general.Candle] + +@pwp_enum +class RiskEngineMessage: + class Initialize: pass + + class WatchList: + watchlist: list[general.MarketItem] + + class Command: + command: str + args: list[str] + + class Signal: + signal: general.Signal \ No newline at end of file From 0d5a8c2f6e4faedf7a1172fde64e9d49df04c66c Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 06:46:59 +0200 Subject: [PATCH 09/20] Decimal --- src/adv_types.py | 30 +++++++++++++++++------------- src/general.py | 10 +++++----- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/adv_types.py b/src/adv_types.py index c729109..3ee3449 100644 --- a/src/adv_types.py +++ b/src/adv_types.py @@ -1,11 +1,12 @@ import struct +from decimal import Decimal -def pwp_number(fmt): +def pwp_number(fmt, value_type): size = struct.calcsize(fmt) def decorator(cls): def __init__(self, value): - self.value = value + self.value = value_type(value) def to_com(self): return struct.pack(fmt, self.value) @@ -24,34 +25,37 @@ def pwp_number(fmt): return decorator # Unsigned integers -@pwp_number(" Date: Tue, 28 Jul 2026 06:49:43 +0200 Subject: [PATCH 10/20] Decimal --- src/adv_types.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/adv_types.py b/src/adv_types.py index 3ee3449..60c24d3 100644 --- a/src/adv_types.py +++ b/src/adv_types.py @@ -57,5 +57,30 @@ class f32: pass @pwp_number(" Date: Tue, 28 Jul 2026 14:38:53 +0200 Subject: [PATCH 11/20] Improved typing --- src/__init__.py | 29 ++++++++++++++++++++++++++- src/pwp.py | 53 +++++++++++++++++++++---------------------------- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 1ec6b30..55cd644 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,7 @@ import sys import struct +import plugin +import general class PulseWire: def __init__(self): @@ -30,4 +32,29 @@ class PulseWire: if len(buffer) != length: raise EOFError("Unexpected EOF while reading payload") - self.on_raw_engine_request(buffer) \ No newline at end of file + self.on_raw_engine_request(buffer) + +class Strategy(PulseWire): + def on_request(self, req: plugin.StrategyEngineMessage): + pass + + def send(self, req: plugin.StrategyMessage): + 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 on_raw_engine_request(self, data: bytes): + com = plugin.StrategyEngineMessage.from_com(data) + + +def test(req: plugin.StrategyMessage.type): + print(req, plugin.StrategyMessage.type) + pass + +test(plugin.StrategyMessage.GetWatchList()) diff --git a/src/pwp.py b/src/pwp.py index 3981724..5fa2f11 100644 --- a/src/pwp.py +++ b/src/pwp.py @@ -1,8 +1,13 @@ import struct import typing -from typing import get_type_hints, get_origin, get_args +from typing import Protocol, Type, TypeVar, Union, get_type_hints, get_origin, get_args from adv_types import * +# A Protocol representing the instance methods added by @pwp +class PulseWire(Protocol): + def to_com(self) -> bytes: ... + def from_com(self, data: bytes, offset: int = 0) -> tuple[typing.Any, int]: ... + class PWPError(Exception): pass @@ -148,81 +153,69 @@ def decode_value(data: bytes, offset: int, typ): raise PWPError(f"Unsupported type: {typ}") - -def pwp(cls): +T = TypeVar("T", bound=Type[typing.Any]) +def pwp(cls: T) -> T: fields = get_type_hints(cls) - def to_com(self): + def to_com(self) -> bytes: output = b"" - for name, typ in fields.items(): output += encode_value(getattr(self, name), typ) - return output @classmethod - def from_com(cls, data): + def from_com(cls, data: bytes, offset: int = 0) -> tuple[typing.Any, int]: values = {} - 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 + return obj, offset - cls.to_com = to_com + cls.to_com = to_com cls.from_com = from_com - return cls + return typing.cast(T, cls) -def pwp_enum(cls): +def pwp_enum(cls: T) -> T: variants = {} - index = 0 + for idx, (name, value) in enumerate(cls.__dict__.items()): if isinstance(value, type) and value.__module__ == cls.__module__: value.__annotations__ = { "_id": u8, **getattr(value, "__annotations__", {}) } - value._id = u8(index) - value = pwp(value) - - variants[idx] = value + variants[index] = value # Safely align variant registration index setattr(cls, name, value) - index += 1 @classmethod - def from_com(enum_cls, data): - enum_id = data[0] + def from_com(enum_cls, data: bytes, offset: int = 0) -> tuple[typing.Any, int]: + enum_id = data[offset] + offset += 1 if enum_id not in variants: raise PWPError(f"Unknown enum id: {enum_id}") variant = variants[enum_id] - - # Decode the rest of the data obj = variant.__new__(variant) - - offset = 1 # skip enum id byte - fields = get_type_hints(variant) for name, typ in fields.items(): if name == "_id": continue - value, offset = decode_value(data, offset, typ) setattr(obj, name, value) - return obj + return obj, offset - cls.from_com = from_com + cls.from_com = from_com # type: ignore + cls.type = Union[tuple(variants.values())] # type: ignore - return cls \ No newline at end of file + return typing.cast(T, cls) \ No newline at end of file From aea78778f6c9e87c22fd57e342acd8157e5f100b Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 15:39:35 +0200 Subject: [PATCH 12/20] Improved typing --- src/__init__.py | 11 ++---- src/adv_types.py | 53 +++++++++++++++++++---------- src/general.py | 18 +++++----- src/plugin.py | 87 +++++++++++++++++++++++++++++++++--------------- src/pwp.py | 25 +++++++------- 5 files changed, 119 insertions(+), 75 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 55cd644..bb8ad77 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -38,7 +38,7 @@ class Strategy(PulseWire): def on_request(self, req: plugin.StrategyEngineMessage): pass - def send(self, req: plugin.StrategyMessage): + def send(self, req: plugin.StrategyMessageType): self.send_raw(req.to_com()) def log(self, log: general.EventLog): @@ -50,11 +50,4 @@ class Strategy(PulseWire): self.send(plugin.StrategyMessage.GetWatchList()) def on_raw_engine_request(self, data: bytes): - com = plugin.StrategyEngineMessage.from_com(data) - - -def test(req: plugin.StrategyMessage.type): - print(req, plugin.StrategyMessage.type) - pass - -test(plugin.StrategyMessage.GetWatchList()) + com = plugin.StrategyEngineMessage.from_com(data=data) diff --git a/src/adv_types.py b/src/adv_types.py index 60c24d3..39d6c9e 100644 --- a/src/adv_types.py +++ b/src/adv_types.py @@ -1,13 +1,32 @@ import struct from decimal import Decimal +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): - def __init__(self, value): - self.value = value_type(value) - + def decorator(cls: B) -> B: def to_com(self): return struct.pack(fmt, self.value) @@ -16,7 +35,6 @@ def pwp_number(fmt, value_type): value = struct.unpack_from(fmt, data, offset)[0] return cls(value), offset + size - cls.__init__ = __init__ cls.to_com = to_com cls.from_com = from_com @@ -26,38 +44,38 @@ def pwp_number(fmt, value_type): # Unsigned integers @pwp_number(" tuple[typing.Any, int]: integer, scale = struct.unpack_from(" bytes: ... - def from_com(self, data: bytes, offset: int = 0) -> tuple[typing.Any, int]: ... + 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 @@ -153,7 +156,7 @@ def decode_value(data: bytes, offset: int, typ): raise PWPError(f"Unsupported type: {typ}") -T = TypeVar("T", bound=Type[typing.Any]) +T = TypeVar("T", bound=type[PulseWire]) def pwp(cls: T) -> T: fields = get_type_hints(cls) @@ -177,18 +180,15 @@ def pwp(cls: T) -> T: cls.to_com = to_com cls.from_com = from_com - return typing.cast(T, cls) + return cls def pwp_enum(cls: T) -> T: variants = {} index = 0 - for idx, (name, value) in enumerate(cls.__dict__.items()): + for name, value in cls.__dict__.items(): if isinstance(value, type) and value.__module__ == cls.__module__: - value.__annotations__ = { - "_id": u8, - **getattr(value, "__annotations__", {}) - } + value.__annotations__["_id"] = u8 value._id = u8(index) value = pwp(value) variants[index] = value # Safely align variant registration index @@ -215,7 +215,6 @@ def pwp_enum(cls: T) -> T: return obj, offset - cls.from_com = from_com # type: ignore - cls.type = Union[tuple(variants.values())] # type: ignore + cls.from_com = from_com - return typing.cast(T, cls) \ No newline at end of file + return cls \ No newline at end of file From 2b26ce46fca71b50038ed4cf796d28db1ae8bf59 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 15:54:17 +0200 Subject: [PATCH 13/20] Improved pwp --- src/__init__.py | 45 ++++- src/plugin.py | 4 +- src/pwp.py | 439 +++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 408 insertions(+), 80 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index bb8ad77..c2d43b9 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -49,5 +49,48 @@ class Strategy(PulseWire): def getWatchList(self): self.send(plugin.StrategyMessage.GetWatchList()) + def getCandleStick(self, symbol: str, interval: general.CandleInterval, 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) + def on_raw_engine_request(self, data: bytes): - com = plugin.StrategyEngineMessage.from_com(data=data) + self.on_request(plugin.StrategyEngineMessage.from_com(data=data)) #type: ignore + +class Risk(PulseWire): + def on_request(self, req: plugin.RiskEngineMessage): + pass + + 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) + + def on_raw_engine_request(self, data: bytes): + com = plugin.RiskEngineMessage.from_com(data=data) diff --git a/src/plugin.py b/src/plugin.py index e8e7eb4..fa3716b 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -20,7 +20,7 @@ class StrategyMessage(PulseWire): class RequestCandlestick(PulseWire): symbol: str - interval: u8 + interval: general.CandleInterval count: u32 class Subscribe(PulseWire): @@ -39,7 +39,7 @@ class RiskMessage(PulseWire): class Log(PulseWire): log: general.EventLog - class GetWatchList: pass + class GetWatchList(PulseWire): pass class Approve(PulseWire): signal: general.Signal diff --git a/src/pwp.py b/src/pwp.py index d5b5f00..dcbecbf 100644 --- a/src/pwp.py +++ b/src/pwp.py @@ -1,12 +1,22 @@ import struct import typing -from typing import Protocol, TypeVar, Union, get_type_hints, get_origin, get_args +import types + +from typing import ( + Protocol, + TypeVar, + get_type_hints, + get_origin, + get_args, +) + from adv_types import * -# A Protocol representing the instance methods added by @pwp + 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) @@ -14,22 +24,35 @@ class PulseWire(Protocol): 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 is typing.Union: + if origin in (typing.Union, types.UnionType): args = get_args(typ) if type(None) in args: if value is None: return b"\x00" - real_type = next(t for t in args if t is not type(None)) - return b"\x01" + encode_value(value, real_type) + real = next( + x for x in args + if x is not type(None) + ) + + return b"\x01" + encode_value(value, real) # list[T] - elif origin is list: + if origin is list: item_type = get_args(typ)[0] out = len(value).to_bytes(4, "little") @@ -39,7 +62,18 @@ def encode_value(value, typ) -> bytes: return out - # dict[K, V] + # tuple[T] + if origin is tuple: + args = get_args(typ) + + out = len(value).to_bytes(4, "little") + + for item, item_type in zip(value, args): + out += encode_value(item, item_type) + + return out + + # dict[K,V] if origin is dict: key_type, value_type = get_args(typ) @@ -51,13 +85,11 @@ def encode_value(value, typ) -> bytes: return out - # primitives - if typ is str: - data = value.encode("utf-8") - return len(data).to_bytes(4, "little") + data - if typ is bytes: - return len(value).to_bytes(4, "little") + value + # primitives + + if typ is bool: + return b"\x01" if value else b"\x00" if typ is int: return struct.pack(" bytes: if typ is float: return struct.pack(" T: + fields = get_type_hints(cls) - def to_com(self) -> bytes: - output = b"" + + def to_com(self): + + out = b"" + for name, typ in fields.items(): - output += encode_value(getattr(self, name), typ) - return output + + 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: bytes, offset: int = 0) -> tuple[typing.Any, int]: - values = {} + 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) + + value, offset = decode_value( + data, + offset, + typ + ) + + setattr( + obj, + name, + value + ) return obj, offset - cls.to_com = to_com + + + cls.to_com = to_com cls.from_com = from_com return cls + + def pwp_enum(cls: T) -> T: + variants = {} + index = 0 - - for name, value in cls.__dict__.items(): - if isinstance(value, type) and value.__module__ == cls.__module__: - value.__annotations__["_id"] = u8 - value._id = u8(index) - value = pwp(value) - variants[index] = value # Safely align variant registration index - setattr(cls, name, value) + + + 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") + + PulseWire.to_com(self) + ) + + raise PWPError( + f"Unknown enum variant {type(self)}" + ) + + + @classmethod - def from_com(enum_cls, data: bytes, offset: int = 0) -> tuple[typing.Any, int]: + 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}") + raise PWPError( + f"Unknown enum id {enum_id}" + ) - variant = variants[enum_id] - obj = variant.__new__(variant) - fields = get_type_hints(variant) - for name, typ in fields.items(): - if name == "_id": - continue - value, offset = decode_value(data, offset, typ) - setattr(obj, name, value) + return variants[enum_id].from_com( + data, + offset + ) - return obj, offset + cls.to_com = to_com cls.from_com = from_com return cls \ No newline at end of file From 7764cf8f7ab59e02931ea98bca28066206077baa Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 16:53:15 +0200 Subject: [PATCH 14/20] Working protocol --- src/__init__.py | 15 ++++++++++-- src/general.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++- src/plugin.py | 4 ++-- src/pwp.py | 4 ++-- src/units.py | 10 ++++---- 5 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index c2d43b9..8c2f2a1 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -36,7 +36,7 @@ class PulseWire: class Strategy(PulseWire): def on_request(self, req: plugin.StrategyEngineMessage): - pass + ... def send(self, req: plugin.StrategyMessageType): self.send_raw(req.to_com()) @@ -69,7 +69,7 @@ class Strategy(PulseWire): class Risk(PulseWire): def on_request(self, req: plugin.RiskEngineMessage): - pass + ... def send(self, req: plugin.RiskMessageType): self.send_raw(req.to_com()) @@ -94,3 +94,14 @@ class Risk(PulseWire): def on_raw_engine_request(self, data: bytes): com = plugin.RiskEngineMessage.from_com(data=data) + +class Bro(Strategy): + def __init__(self): + self.log(general.EventLog.info("bro", "Hello, World")) + super().__init__() + + def on_request(self, req: plugin.StrategyEngineMessage): + # self.log(general.EventLog.info("bro", "Hello, World")) + pass + +Bro().start() \ No newline at end of file diff --git a/src/general.py b/src/general.py index 137955a..e9a773d 100644 --- a/src/general.py +++ b/src/general.py @@ -1,6 +1,7 @@ from pwp import pwp, pwp_enum, PulseWire from adv_types import * import units +from typing import TypeAlias @pwp_enum class MarketTrend(PulseWire): @@ -42,12 +43,73 @@ class Signal(PulseWire): 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: LogKind + 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 diff --git a/src/plugin.py b/src/plugin.py index fa3716b..707f353 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -63,7 +63,7 @@ class StrategyEngineMessage(PulseWire): interval: general.CandleInterval candle: general.Candle - class CandleStick(PulseWire): + class Candlestick(PulseWire): symbol: str interval: general.CandleInterval candles: list[general.Candle] @@ -105,7 +105,7 @@ StrategyEngineMessageType: TypeAlias = ( | StrategyEngineMessage.WatchList | StrategyEngineMessage.Command | StrategyEngineMessage.CandleUpdate - | StrategyEngineMessage.CandleStick + | StrategyEngineMessage.Candlestick ) RiskEngineMessageType: TypeAlias = ( diff --git a/src/pwp.py b/src/pwp.py index dcbecbf..939f56d 100644 --- a/src/pwp.py +++ b/src/pwp.py @@ -461,7 +461,7 @@ def pwp_enum(cls: T) -> T: return ( idx.to_bytes(1, "little") - + PulseWire.to_com(self) + + self.to_com() ) raise PWPError( @@ -484,7 +484,7 @@ def pwp_enum(cls: T) -> T: ) enum_id = data[offset] - offset += 1 + # offset += 1 if enum_id not in variants: diff --git a/src/units.py b/src/units.py index bc60577..0f49203 100644 --- a/src/units.py +++ b/src/units.py @@ -1,21 +1,21 @@ -from pwp import pwp, pwp_enum +from pwp import pwp, pwp_enum, PulseWire from adv_types import * @pwp -class Symbol: +class Symbol(PulseWire): unit: str @pwp -class USD: +class USD(PulseWire): unit: f64 @pwp_enum -class Direction: +class Direction(PulseWire): class Buy: pass class Sell: pass @pwp_enum -class Volatility: +class Volatility(PulseWire): class Low: pass class Medium: pass class High: pass From bc28ac73015ffe85a6f09fdae272d52eaf32d699 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 17:33:57 +0200 Subject: [PATCH 15/20] Working strategy --- src/__init__.py | 83 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 8c2f2a1..359fe89 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,4 @@ import sys -import struct import plugin import general @@ -7,7 +6,7 @@ class PulseWire: def __init__(self): ... - def on_raw_engine_request(self, data: bytes): + def onRaw(self, data: bytes): ... def send_raw(self, data: bytes): @@ -17,25 +16,42 @@ class PulseWire: def start(self): while True: - len_buf = sys.stdin.buffer.read(struct.calcsize("P")) + len_buf = sys.stdin.buffer.read(8) + + sys.stderr.write(str(len_buf)) if len(len_buf) == 0: - return None + continue length = int.from_bytes(len_buf, "little") if length == 0: - return None + continue buffer = sys.stdin.buffer.read(length) if len(buffer) != length: raise EOFError("Unexpected EOF while reading payload") - self.on_raw_engine_request(buffer) + self.onRaw(buffer) class Strategy(PulseWire): - def on_request(self, req: 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 on(self, req: plugin.StrategyEngineMessageType): ... def send(self, req: plugin.StrategyMessageType): @@ -64,11 +80,35 @@ class Strategy(PulseWire): msg.signal = signal self.send(msg) - def on_raw_engine_request(self, data: bytes): - self.on_request(plugin.StrategyEngineMessage.from_com(data=data)) #type: ignore + def onRaw(self, data: bytes): + result: tuple[plugin.StrategyEngineMessageType, int] = plugin.StrategyEngineMessage.from_com(data=data) + req, _ = result + + self.on(req) + + handler = getattr( + self, + f"on{req.__class__.__name__}", + None + ) + + if handler: + handler(req) class Risk(PulseWire): - def on_request(self, req: 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 on(self, req: plugin.RiskEngineMessageType): ... def send(self, req: plugin.RiskMessageType): @@ -92,16 +132,27 @@ class Risk(PulseWire): msg.reason = reason self.send(msg) - def on_raw_engine_request(self, data: bytes): - com = plugin.RiskEngineMessage.from_com(data=data) + def onRaw(self, data: bytes): + result: tuple[plugin.RiskEngineMessageType, int] = plugin.RiskEngineMessage.from_com(data=data) + req, _ = result + + self.on(req) + + handler = getattr( + self, + f"on{req.__class__.__name__}", + None + ) + + if handler: + handler(req) class Bro(Strategy): def __init__(self): - self.log(general.EventLog.info("bro", "Hello, World")) super().__init__() - def on_request(self, req: plugin.StrategyEngineMessage): - # self.log(general.EventLog.info("bro", "Hello, World")) - pass + def onInitialize(self, msg: plugin.StrategyEngineMessage.Initialize): + self.log(general.EventLog.info("bro", "Hello, World")) + return super().onInitialize(msg) Bro().start() \ No newline at end of file From e5325fbd964b69f419a8f7f4bae6e8af1e6e33cb Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 17:52:03 +0200 Subject: [PATCH 16/20] Better architecture --- src/__init__.py | 90 ++++++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 57 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 359fe89..bd214e0 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,12 +1,16 @@ -import sys import plugin +import sys +from typing import Any, Generic, TypeVar, Type, Optional import general -class PulseWire: - def __init__(self): - ... +T = TypeVar("T", bound=general.PulseWire) +TT = TypeVar("TT", bound=general.PulseWire) - def onRaw(self, data: bytes): +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): @@ -18,24 +22,39 @@ class PulseWire: while True: len_buf = sys.stdin.buffer.read(8) - sys.stderr.write(str(len_buf)) + # Standard EOF check: zero bytes returned from read means stdin closed + if not len_buf: + break - if len(len_buf) == 0: - continue + 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) -class Strategy(PulseWire): + 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): ... @@ -51,9 +70,6 @@ class Strategy(PulseWire): def onCandlestick(self, msg: plugin.StrategyEngineMessage.Candlestick): ... - def on(self, req: plugin.StrategyEngineMessageType): - ... - def send(self, req: plugin.StrategyMessageType): self.send_raw(req.to_com()) @@ -80,22 +96,10 @@ class Strategy(PulseWire): msg.signal = signal self.send(msg) - def onRaw(self, data: bytes): - result: tuple[plugin.StrategyEngineMessageType, int] = plugin.StrategyEngineMessage.from_com(data=data) - req, _ = result +class Risk(PulseWire[plugin.RiskEngineMessage, plugin.RiskEngineMessageType]): + def __init__(self): + super().__init__(plugin.RiskEngineMessage) - self.on(req) - - handler = getattr( - self, - f"on{req.__class__.__name__}", - None - ) - - if handler: - handler(req) - -class Risk(PulseWire): def onInitialize(self, msg: plugin.RiskEngineMessage.Initialize): ... @@ -108,9 +112,6 @@ class Risk(PulseWire): def onSignal(self, msg: plugin.RiskEngineMessage.Signal): ... - def on(self, req: plugin.RiskEngineMessageType): - ... - def send(self, req: plugin.RiskMessageType): self.send_raw(req.to_com()) @@ -131,28 +132,3 @@ class Risk(PulseWire): msg = plugin.RiskMessage.Reject() msg.reason = reason self.send(msg) - - def onRaw(self, data: bytes): - result: tuple[plugin.RiskEngineMessageType, int] = plugin.RiskEngineMessage.from_com(data=data) - req, _ = result - - self.on(req) - - handler = getattr( - self, - f"on{req.__class__.__name__}", - None - ) - - if handler: - handler(req) - -class Bro(Strategy): - def __init__(self): - super().__init__() - - def onInitialize(self, msg: plugin.StrategyEngineMessage.Initialize): - self.log(general.EventLog.info("bro", "Hello, World")) - return super().onInitialize(msg) - -Bro().start() \ No newline at end of file From 3132d85bddfb441315fce2580678786a422f0739 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 18:12:10 +0200 Subject: [PATCH 17/20] Library structure --- src/{ => pulse_sdk_py}/__init__.py | 7 ++++--- src/{ => pulse_sdk_py}/adv_types.py | 2 +- src/{ => pulse_sdk_py}/general.py | 7 ++++--- src/{ => pulse_sdk_py}/plugin.py | 8 ++++---- src/{ => pulse_sdk_py}/pwp.py | 3 +-- src/{ => pulse_sdk_py}/units.py | 4 ++-- 6 files changed, 16 insertions(+), 15 deletions(-) rename src/{ => pulse_sdk_py}/__init__.py (97%) rename src/{ => pulse_sdk_py}/adv_types.py (100%) rename src/{ => pulse_sdk_py}/general.py (97%) rename src/{ => pulse_sdk_py}/plugin.py (96%) rename src/{ => pulse_sdk_py}/pwp.py (99%) rename src/{ => pulse_sdk_py}/units.py (80%) diff --git a/src/__init__.py b/src/pulse_sdk_py/__init__.py similarity index 97% rename from src/__init__.py rename to src/pulse_sdk_py/__init__.py index bd214e0..9a136f7 100644 --- a/src/__init__.py +++ b/src/pulse_sdk_py/__init__.py @@ -1,7 +1,8 @@ -import plugin import sys -from typing import Any, Generic, TypeVar, Type, Optional -import general +from typing import Generic, TypeVar, Type + +from . import general +from . import plugin T = TypeVar("T", bound=general.PulseWire) TT = TypeVar("TT", bound=general.PulseWire) diff --git a/src/adv_types.py b/src/pulse_sdk_py/adv_types.py similarity index 100% rename from src/adv_types.py rename to src/pulse_sdk_py/adv_types.py index 39d6c9e..b892c89 100644 --- a/src/adv_types.py +++ b/src/pulse_sdk_py/adv_types.py @@ -1,5 +1,5 @@ -import struct from decimal import Decimal +import struct import typing T = typing.TypeVar("T") diff --git a/src/general.py b/src/pulse_sdk_py/general.py similarity index 97% rename from src/general.py rename to src/pulse_sdk_py/general.py index e9a773d..2715226 100644 --- a/src/general.py +++ b/src/pulse_sdk_py/general.py @@ -1,8 +1,9 @@ -from pwp import pwp, pwp_enum, PulseWire -from adv_types import * -import units 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 diff --git a/src/plugin.py b/src/pulse_sdk_py/plugin.py similarity index 96% rename from src/plugin.py rename to src/pulse_sdk_py/plugin.py index 707f353..a5f2a08 100644 --- a/src/plugin.py +++ b/src/pulse_sdk_py/plugin.py @@ -1,9 +1,9 @@ -from pwp import pwp, pwp_enum, PulseWire -from adv_types import * -import units -import general 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 diff --git a/src/pwp.py b/src/pulse_sdk_py/pwp.py similarity index 99% rename from src/pwp.py rename to src/pulse_sdk_py/pwp.py index 939f56d..928e8ca 100644 --- a/src/pwp.py +++ b/src/pulse_sdk_py/pwp.py @@ -1,7 +1,6 @@ import struct import typing import types - from typing import ( Protocol, TypeVar, @@ -10,7 +9,7 @@ from typing import ( get_args, ) -from adv_types import * +from .adv_types import * class PulseWire(Protocol): diff --git a/src/units.py b/src/pulse_sdk_py/units.py similarity index 80% rename from src/units.py rename to src/pulse_sdk_py/units.py index 0f49203..11cf4d4 100644 --- a/src/units.py +++ b/src/pulse_sdk_py/units.py @@ -1,5 +1,5 @@ -from pwp import pwp, pwp_enum, PulseWire -from adv_types import * +from .pwp import pwp, pwp_enum, PulseWire +from .adv_types import * @pwp class Symbol(PulseWire): From c18dc14c5412938c89007ec58e0eeb48802ed641 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 18:22:48 +0200 Subject: [PATCH 18/20] pyproject --- pyproject.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a21cf81 --- /dev/null +++ b/pyproject.toml @@ -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"} +] \ No newline at end of file From 8cc5f389f94f8a4dabd861f72ff89d78232047dd Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 28 Jul 2026 19:44:58 +0200 Subject: [PATCH 19/20] Fix --- src/pulse_sdk_py/pwp.py | 280 +++++++--------------------------------- 1 file changed, 44 insertions(+), 236 deletions(-) diff --git a/src/pulse_sdk_py/pwp.py b/src/pulse_sdk_py/pwp.py index 928e8ca..ae40c01 100644 --- a/src/pulse_sdk_py/pwp.py +++ b/src/pulse_sdk_py/pwp.py @@ -4,6 +4,7 @@ import types from typing import ( Protocol, TypeVar, + Any, get_type_hints, get_origin, get_args, @@ -38,313 +39,120 @@ def encode_value(value, typ) -> bytes: # 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) - ) - + real = next(x for x in args if x is not type(None)) return b"\x01" + encode_value(value, real) - # list[T] - if origin is list: - item_type = get_args(typ)[0] - - out = len(value).to_bytes(4, "little") - - for item in value: - out += encode_value(item, item_type) - - return out - - # tuple[T] - if origin is tuple: + # 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 - for item, item_type in zip(value, args): + # 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: - key_type, value_type = get_args(typ) - + # 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) - out += encode_value(v, value_type) - + 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 - + # Primitives if typ is bool: return b"\x01" if value else b"\x00" - if typ is int: return struct.pack(" Date: Tue, 28 Jul 2026 20:15:11 +0200 Subject: [PATCH 20/20] Fix --- src/pulse_sdk_py/__init__.py | 2 +- src/pulse_sdk_py/adv_types.py | 45 ++++++++++++++++++++++++++++------- src/pulse_sdk_py/general.py | 28 +++++++++++----------- src/pulse_sdk_py/plugin.py | 2 +- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/pulse_sdk_py/__init__.py b/src/pulse_sdk_py/__init__.py index 9a136f7..3be7b31 100644 --- a/src/pulse_sdk_py/__init__.py +++ b/src/pulse_sdk_py/__init__.py @@ -82,7 +82,7 @@ class Strategy(PulseWire[plugin.StrategyEngineMessage, plugin.StrategyEngineMess def getWatchList(self): self.send(plugin.StrategyMessage.GetWatchList()) - def getCandleStick(self, symbol: str, interval: general.CandleInterval, count: general.u32): + def getCandleStick(self, symbol: str, interval: general.CandleIntervalType, count: general.u32): msg = plugin.StrategyMessage.RequestCandlestick() msg.symbol = symbol msg.interval = interval diff --git a/src/pulse_sdk_py/adv_types.py b/src/pulse_sdk_py/adv_types.py index b892c89..d2f163b 100644 --- a/src/pulse_sdk_py/adv_types.py +++ b/src/pulse_sdk_py/adv_types.py @@ -76,30 +76,59 @@ class f32(PulseUnit): pass class f64(PulseUnit): pass class decimal(PulseUnit): - size = struct.calcsize(" bytes: sign, digits, exponent = self.value.as_tuple() if not isinstance(exponent, int): raise ValueError("Cannot serialize NaN or Infinity") scale = -exponent - integer = int("".join(map(str, digits))) + 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: - integer = -integer + flags |= 0x80000000 - - return struct.pack(" tuple[typing.Any, int]: - integer, scale = struct.unpack_from("> 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 \ No newline at end of file diff --git a/src/pulse_sdk_py/general.py b/src/pulse_sdk_py/general.py index 2715226..5d345a3 100644 --- a/src/pulse_sdk_py/general.py +++ b/src/pulse_sdk_py/general.py @@ -19,20 +19,20 @@ class LogKind(PulseWire): @pwp_enum class CandleInterval(PulseWire): - class OneMinute: pass - class ThreeMinutes: pass - class FiveMinutes: pass - class FifteenMinutes: pass - class ThirtyMinutes: pass - class OneHour: pass - class TwoHours: pass - class FourHours: pass - class EightHours: pass - class TwelveHours: pass - class OneDay: pass - class ThreeDays: pass - class OneWeek: pass - class OneMonth: pass + 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): diff --git a/src/pulse_sdk_py/plugin.py b/src/pulse_sdk_py/plugin.py index a5f2a08..679e975 100644 --- a/src/pulse_sdk_py/plugin.py +++ b/src/pulse_sdk_py/plugin.py @@ -20,7 +20,7 @@ class StrategyMessage(PulseWire): class RequestCandlestick(PulseWire): symbol: str - interval: general.CandleInterval + interval: general.CandleIntervalType count: u32 class Subscribe(PulseWire):