Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion data_generator/base_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ def debug_log(self):
# Log the prices
formatted_prices = {}
for tp, price_source in self.latest_websocket_events.items():
if TradePair.get_latest_tade_pair_from_trade_pair_str(tp).is_forex:
if TradePair.get_latest_trade_pair_from_trade_pair_str(tp).is_forex:
formatted_prices[tp] = f"({price_source.bid:.5f}/{price_source.ask:.5f})"
else:
formatted_prices[tp] = f"{price_source.close:.2f}"
Expand Down
61 changes: 29 additions & 32 deletions data_generator/polygon_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,11 @@ def _subscribe_websockets(self, tpc: TradePairCategory = None):

def symbol_to_trade_pair(self, symbol: str):
# Should work for indices and forex
tp = TradePair.get_latest_tade_pair_from_trade_pair_str(symbol)
tp = TradePair.get_latest_trade_pair_from_trade_pair_str(symbol)
if tp:
return tp
# Should work for crypto. Anything else will return None
tp = TradePair.get_latest_tade_pair_from_trade_pair_str(symbol.replace('-', '/'))
tp = TradePair.get_latest_trade_pair_from_trade_pair_str(symbol.replace('-', '/'))
if not tp:
raise ValueError(f"Unknown symbol: {symbol}")
return tp
Expand Down Expand Up @@ -876,50 +876,47 @@ def get_candles_for_trade_pair(

return aggs

def get_quote(self, trade_pair: TradePair, processed_ms: int) -> (float, float, int):
def get_quote(self, processed_ms: int, trade_pair: TradePair=None, polygon_ticker: str=None) -> (float, float, int):
"""
returns the bid and ask quote for a trade_pair at processed_ms
"""
# polygon_ticker = self.trade_pair_to_polygon_ticker(trade_pair)

if self.POLYGON_CLIENT is None:
self.instantiate_not_pickleable_objects()

if trade_pair.is_forex or trade_pair.is_equities:
if not polygon_ticker:
if trade_pair.is_crypto or trade_pair.is_indices:
return 0, 0, 0 # TODO: (unused) quotes for crypto, indices
polygon_ticker = self.trade_pair_to_polygon_ticker(trade_pair)
quotes = self.POLYGON_CLIENT.list_quotes(
ticker=polygon_ticker,
timestamp_lte=processed_ms * 1_000_000,
sort="participant_timestamp",
order="desc",
limit=1
)
for q in quotes:
return q.bid_price, q.ask_price, int(q.participant_timestamp/1_000_000) # convert ns back to ms
else:
# crypto
return 0, 0, 0

def get_currency_conversion(self, trade_pair: TradePair=None, base: str=None, quote: str=None) -> float:
quotes = self.POLYGON_CLIENT.list_quotes(
ticker=polygon_ticker,
timestamp_lte=processed_ms * 1_000_000,
sort="participant_timestamp",
order="desc",
limit=1
)
for q in quotes:
return q.bid_price, q.ask_price, int(q.participant_timestamp/1_000_000) # convert ns back to ms

def get_currency_conversion(self, base: str, quote: str, time_ms: int=None) -> float:
"""
get the currency conversion rate from base currency to quote currency
"""
if self.POLYGON_CLIENT is None:
self.instantiate_not_pickleable_objects()

if not (base and quote):
if trade_pair and trade_pair.is_forex:
base, quote = trade_pair.trade_pair.split("/")
else:
raise ValueError("Must provide either a valid forex pair or a base and quote for currency conversion")
if not time_ms:
time_ms = TimeUtil.now_in_millis()

rate = self.POLYGON_CLIENT.get_real_time_currency_conversion(
from_=base,
to=quote,
precision=4,
)
polygon_ticker = 'C:' + base + quote
bid, _, _ = self.get_quote(time_ms, polygon_ticker=polygon_ticker)
return bid

return rate.converted
def get_base_to_usd_conversion(self, base: str, time_ms: int=None) -> float:
"""
get the currency conversion rate from base currency to USD
"""
if base == "USD":
return 1
return self.get_currency_conversion(base=base, quote="USD", time_ms=time_ms)


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions data_generator/tiingo_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,11 @@ def process_ps_from_websocket(self, tp: TradePair, ps1: PriceSource):

def symbol_to_trade_pair(self, symbol: str):
# Should work for indices and forex
tp = TradePair.get_latest_tade_pair_from_trade_pair_str(symbol)
tp = TradePair.get_latest_trade_pair_from_trade_pair_str(symbol)
if tp:
return tp
# Should work for crypto. Anything else will return None
tp = TradePair.get_latest_tade_pair_from_trade_pair_str(symbol.replace('-', '/'))
tp = TradePair.get_latest_trade_pair_from_trade_pair_str(symbol.replace('-', '/'))
if not tp:
raise ValueError(f"Unknown symbol: {symbol}")
return tp
Expand Down
6 changes: 4 additions & 2 deletions mining/run_receive_signals_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ def handle_data():
raise Exception("trade_pair must be a string or a dict")

signal = Signal(trade_pair=TradePair.from_trade_pair_id(signal_trade_pair_str),
leverage=float(data["leverage"]),
leverage=float(data["leverage"]) if data.get("leverage") is not None else None,
value=float(data["value"]) if data.get("value") is not None else None,
volume=float(data["volume"]) if data.get("volume") is not None else None,
order_type=OrderType.from_string(data["order_type"].upper()))
# make miner received signals dir if doesnt exist
ValiBkpUtils.make_dir(MinerConfig.get_miner_received_signals_dir())
Expand All @@ -83,4 +85,4 @@ def handle_data():

if __name__ == "__main__":
waitress.serve(app, host="0.0.0.0", port=80, connection_limit=1000)
print('Successfully started run_receive_signals_server.')
print('Successfully started run_receive_signals_server.')
7 changes: 5 additions & 2 deletions mining/sample_signal_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ def default(self, obj):
url = f'{base_url}/api/receive-signal'

# Define the JSON data to be sent in the request
# Note: You must provide exactly ONE of 'leverage', 'value', or 'volume'
data = {
'trade_pair': TradePair.BTCUSD,
'order_type': OrderType.LONG,
'leverage': .1,
'leverage': 0.1, # leverage
# 'value': 10_000, # USD value
# 'volume': 0.1, # base asset volume (lots, shares, coins, etc.)
'api_key': 'xxxx'
}

Expand All @@ -61,4 +64,4 @@ def default(self, obj):
print(response.json()) # Print the response data
else:
print(response.__dict__)
print("POST request failed with status code:", response.status_code)
print("POST request failed with status code:", response.status_code)
35 changes: 31 additions & 4 deletions neurons/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,30 @@ def parse_miner_uuid(self, synapse: template.protocol.SendSignal):
assert isinstance(temp, str), f"excepted string miner uuid but got {temp}"
return temp

def parse_order_size(self, signal, price, trade_pair, portfolio_value):
"""
parses an order signal and calculates leverage, value, and volume
"""
leverage = signal.get("leverage")
value = signal.get("value")
volume = signal.get("volume")

fields_set = [x is not None for x in (leverage, value, volume)]
if sum(fields_set) != 1:
raise ValueError("Exactly one of 'leverage', 'value', or 'volume' must be set")

if leverage is not None:
value = leverage * portfolio_value
volume = value / (price * trade_pair.lot_size)
elif value is not None:
leverage = value / portfolio_value
volume = value / (price * trade_pair.lot_size)
elif volume is not None:
value = volume * (price * trade_pair.lot_size)
leverage = value / portfolio_value

return leverage, value, volume

# This is the core validator function to receive a signal
def receive_signal(self, synapse: template.protocol.SendSignal,
) -> template.protocol.SendSignal:
Expand Down Expand Up @@ -730,7 +754,6 @@ def receive_signal(self, synapse: template.protocol.SendSignal,
f"Ignoring order for [{miner_hotkey}] due to no live prices being found for trade_pair [{trade_pair}]. Please try again.")
best_price_source = price_sources[0]

signal_leverage = signal["leverage"]
signal_order_type = OrderType.from_string(signal["order_type"])

# Multiple threads can run receive_signal at once. Don't allow two threads to trample each other.
Expand All @@ -745,12 +768,16 @@ def receive_signal(self, synapse: template.protocol.SendSignal,
miner_hotkey, trade_pair_to_open_position, miner_order_uuid)

if existing_position:
price = best_price_source.parse_appropriate_price(now_ms, trade_pair.is_forex, signal_order_type, existing_position)
leverage, value, volume = self.parse_order_size(signal, price, trade_pair, ValiConfig.CAPITAL)

order = Order(
trade_pair=trade_pair,
order_type=signal_order_type,
leverage=signal_leverage,
price=best_price_source.parse_appropriate_price(now_ms, trade_pair.is_forex, signal_order_type,
existing_position),
leverage=leverage,
value=value,
volume=volume,
price=price,
processed_ms=now_ms,
order_uuid=miner_order_uuid if miner_order_uuid else str(uuid.uuid4()),
price_sources=price_sources,
Expand Down
6 changes: 2 additions & 4 deletions vali_objects/position.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,7 @@ def calculate_pnl(self, current_price, t_ms=None, order=None):
# update realized pnl for orders that reduce the size of a position
if (order.order_type != self.position_type or self.position_type == OrderType.FLAT):
exit_price = current_price * (1 + order.slippage) if order.leverage > 0 else current_price * (1 - order.slippage)
order_volume = order.leverage # (order.leverage * ValiConfig.CAPITAL) / order.price # TODO: calculate order.volume as an order attribute
self.realized_pnl += -1 * (exit_price - self.average_entry_price) * order_volume # TODO: FIFO entry cost
self.realized_pnl += -1 * (exit_price - self.average_entry_price) * (order.volume * order.trade_pair.lot_size) # TODO: FIFO entry cost
unrealized_pnl = (current_price - self.average_entry_price) * min(self.net_leverage, self.net_leverage + order.leverage, key=abs)
else:
unrealized_pnl = (current_price - self.average_entry_price) * self.net_leverage
Expand Down Expand Up @@ -573,8 +572,7 @@ def update_position_state_for_new_order(self, order, delta_leverage):
+ entry_price * delta_leverage
) / new_net_leverage

order_volume = order.leverage # (order.leverage * ValiConfig.CAPITAL) / entry_price # TODO: order volume. represents # of shares, etc.
self.cumulative_entry_value += entry_price * order_volume # TODO: replace with order.volume attribute
self.cumulative_entry_value += entry_price * (order.volume * order.trade_pair.lot_size)
self.net_leverage = new_net_leverage

def initialize_position_from_first_order(self, order):
Expand Down
2 changes: 1 addition & 1 deletion vali_objects/utils/elimination_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def handle_perf_ledger_eliminations(self, position_locks):
price_info = e['price_info']
trade_pair_to_price_source_used_for_elimination_check = {}
for k, v in price_info.items():
trade_pair = TradePair.get_latest_tade_pair_from_trade_pair_str(k)
trade_pair = TradePair.get_latest_trade_pair_from_trade_pair_str(k)
elimination_initiated_time_ms = e['elimination_initiated_time_ms']
trade_pair_to_price_source_used_for_elimination_check[trade_pair] = PriceSource(source='elim', open=v,
close=v,
Expand Down
2 changes: 1 addition & 1 deletion vali_objects/utils/live_price_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def get_quote(self, trade_pair: TradePair, processed_ms: int) -> (float, float,
"""
returns the bid and ask quote for a trade_pair at processed_ms. Only Polygon supports point-in-time bid/ask.
"""
return self.polygon_data_service.get_quote(trade_pair, processed_ms)
return self.polygon_data_service.get_quote(processed_ms, trade_pair=trade_pair)

def parse_extreme_price_in_window(self, candle_data: Dict[TradePair, List[PriceSource]], open_position: Position, parse_min: bool = True) -> Tuple[float, PriceSource] | Tuple[None, None]:
trade_pair = open_position.trade_pair
Expand Down
3 changes: 1 addition & 2 deletions vali_objects/utils/price_slippage_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ def calc_slippage_forex(cls, bid:float, ask:float, order:Order) -> float:
# bt.logging.info(f"bid: {bid}, ask: {ask}, adv: {avg_daily_volume}, vol: {annualized_volatility}")

size = abs(order.leverage) * ValiConfig.CAPITAL
base, _ = order.trade_pair.trade_pair.split("/")
base_to_usd_conversion = cls.live_price_fetcher.polygon_data_service.get_currency_conversion(base=base, quote="USD") if base != "USD" else 1 # TODO: fallback?
base_to_usd_conversion = cls.live_price_fetcher.polygon_data_service.get_base_to_usd_conversion(base=order.trade_pair.base, time_ms=order.processed_ms) # TODO: tiingo fallback?
# print(base_to_usd_conversion)
volume_standard_lots = size / (100_000 * base_to_usd_conversion) # Volume expressed in terms of standard lots (1 std lot = 100,000 base currency)

Expand Down
22 changes: 21 additions & 1 deletion vali_objects/vali_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class ValiConfig:
EQUITIES_MIN_LEVERAGE = 0.1
EQUITIES_MAX_LEVERAGE = 3

ORDER_MIN_VALUE = 2000
CAPITAL = 100_000 # conversion of 1x leverage to $100K in capital

MAX_DAILY_DRAWDOWN = 0.95 # Portfolio should never fall below .95 x of initial value when measured day to day
Expand Down Expand Up @@ -310,10 +311,19 @@ def is_forex(self):
@property
def is_equities(self):
return self.trade_pair_category == TradePairCategory.EQUITIES

@property
def is_indices(self):
return self.trade_pair_category == TradePairCategory.INDICES

@property
def lot_size(self):
trade_pair_lot_size = {TradePairCategory.CRYPTO: 1,
TradePairCategory.FOREX: 100_000,
TradePairCategory.INDICES: 1,
TradePairCategory.EQUITIES: 1}
return trade_pair_lot_size[self.trade_pair_category]

@property
def leverage_multiplier(self) -> int:
trade_pair_leverage_multiplier = {TradePairCategory.CRYPTO: 10,
Expand All @@ -322,6 +332,16 @@ def leverage_multiplier(self) -> int:
TradePairCategory.EQUITIES: 2}
return trade_pair_leverage_multiplier[self.trade_pair_category]

@property
def base(self):
if self.is_forex:
return self.trade_pair.split("/")[0]

@property
def quote(self):
if self.is_forex:
return self.trade_pair.split("/")[1]

@classmethod
def categories(cls):
return {tp.trade_pair_id: tp.trade_pair_category.value for tp in cls}
Expand Down Expand Up @@ -392,7 +412,7 @@ def get_latest_trade_pair_from_trade_pair_id(trade_pair_id):
return TRADE_PAIR_ID_TO_TRADE_PAIR.get(trade_pair_id)

@staticmethod
def get_latest_tade_pair_from_trade_pair_str(trade_pair_str):
def get_latest_trade_pair_from_trade_pair_str(trade_pair_str):
return TRADE_PAIR_STR_TO_TRADE_PAIR.get(trade_pair_str)

def __str__(self):
Expand Down
31 changes: 25 additions & 6 deletions vali_objects/vali_dataclasses/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
# Copyright © 2024 Taoshi Inc

from time_util.time_util import TimeUtil
from pydantic import field_validator
from pydantic import field_validator, model_validator

from vali_objects.enums.order_type_enum import OrderType
from vali_objects.vali_config import ValiConfig
from vali_objects.vali_dataclasses.order_signal import Signal
from vali_objects.vali_dataclasses.price_source import PriceSource
from enum import Enum, auto
Expand All @@ -23,16 +24,12 @@ class Order(Signal):
price_sources: list = []
src: int = ORDER_SRC_ORGANIC

@field_validator('price', 'processed_ms', 'leverage', mode='before')
@field_validator('price', 'processed_ms', mode='before')
def validate_values(cls, v, info):
if info.field_name == 'price' and v < 0:
raise ValueError("Price must be greater than 0")
if info.field_name == 'processed_ms' and v < 0:
raise ValueError("processed_ms must be greater than 0")
if info.field_name == 'leverage':
order_type = info.data.get('order_type')
if order_type == OrderType.LONG and v < 0:
raise ValueError("Leverage must be positive for LONG orders.")
return v

@field_validator('order_uuid', mode='before')
Expand All @@ -47,6 +44,26 @@ def validate_price_sources(cls, v):
return [PriceSource(**ps) if isinstance(ps, dict) else ps for ps in v]
return v

@model_validator(mode='before')
def validate_size(cls, values):
"""
Ensure that size meets min and maximum requirements
"""
order_type = values['order_type']
is_flat_order = order_type == OrderType.FLAT or order_type == 'FLAT'
lev = values['leverage']
val = values.get('value')
if not is_flat_order and not (ValiConfig.ORDER_MIN_LEVERAGE <= abs(lev) <= ValiConfig.ORDER_MAX_LEVERAGE):
raise ValueError(
f"Order leverage must be between {ValiConfig.ORDER_MIN_LEVERAGE} and {ValiConfig.ORDER_MAX_LEVERAGE}, provided - lev [{lev}] and order_type [{order_type}] ({type(order_type)})")
if val is not None and not is_flat_order and not ValiConfig.ORDER_MIN_VALUE <= abs(val):
raise ValueError(f"Order value must be greater than {ValiConfig.ORDER_MIN_VALUE}, provided value is {abs(val)}")
return values

@model_validator(mode="before")
def check_exclusive_fields(cls, values):
return values

# Using Pydantic's constructor instead of a custom from_dict method
@classmethod
def from_dict(cls, order_dict):
Expand All @@ -62,6 +79,8 @@ def to_python_dict(self):
return {'trade_pair_id': trade_pair_id,
'order_type': self.order_type.name,
'leverage': self.leverage,
'value': self.value,
'volume': self.volume,
'price': self.price,
'bid': self.bid,
'ask': self.ask,
Expand Down
Loading
Loading