Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a577621
add order value and volume attributes for size
sli-tao May 21, 2025
5d47f1a
use volume attribute for pnl
sli-tao May 21, 2025
9b1a12b
slippage using order size
sli-tao Oct 2, 2025
dc00f05
remove interpolated value
sli-tao Oct 17, 2025
d7cf13c
remove asset split grace period
sli-tao Oct 21, 2025
ff2f86b
remove slippage capital arg
sli-tao Oct 21, 2025
13793aa
Fix after rebase
sli-tao Oct 22, 2025
42c3aaf
calc quantity from signal
sli-tao Oct 27, 2025
0d640f9
calculate pnl from quantity
sli-tao Oct 27, 2025
79b1216
convert position pnl to usd
sli-tao Oct 28, 2025
204160d
update perf ledger realized and unrealized pnl
sli-tao Oct 30, 2025
29d3fc6
remove contract manager and account size from perf ledger
sli-tao Oct 30, 2025
e69b8fe
realtime drawdown slash calculation
sli-tao Oct 30, 2025
8fabc67
grace period for penalty free withdrawals down to 300 theta
sli-tao Oct 30, 2025
3ad6c3e
allow withdrawals down to max_theta. limit slash amount to max_theta
sli-tao Oct 30, 2025
5131f1d
update cost per theta
sli-tao Oct 30, 2025
377b654
use order value as size in slippage model
sli-tao Oct 30, 2025
766c390
update historical order attributes to add quantity and value
sli-tao Oct 30, 2025
fcdaa3d
calculate position leverage, migrate position account size
sli-tao Oct 30, 2025
336b735
update migration
sli-tao Oct 31, 2025
c5b26ae
calc slippage in main process
sli-tao Oct 31, 2025
80ced49
fix loop
sli-tao Oct 31, 2025
ff75eca
fix auto_sync tests
sli-tao Oct 31, 2025
6c64568
add account_size to test positions
sli-tao Oct 31, 2025
5391b7c
fix additional tests
sli-tao Oct 31, 2025
4ad8900
remove incorrect import
sli-tao Oct 31, 2025
de6d6e1
Fix after rebase
sli-tao Nov 3, 2025
e25d1a7
bump version
sli-tao Nov 3, 2025
69780ca
use net realized and unrealized pnl
sli-tao Nov 3, 2025
11232cf
add quote usd conversion to order attribute
sli-tao Nov 3, 2025
6ac7523
condense position usd value pnl tracking
sli-tao Nov 3, 2025
5877553
update acct size time
sli-tao Nov 3, 2025
9f25aa8
fix position
sli-tao Nov 3, 2025
e8e9b01
update usd conversion
sli-tao Nov 3, 2025
e172f81
add usd base trade pairs
sli-tao Nov 3, 2025
ddf0905
Fix after rebase
sli-tao Nov 5, 2025
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 meta/meta.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"subnet_version": "7.0.2"
"subnet_version": "7.0.3"
}
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,
quantity=float(data["quantity"]) if data.get("quantity") 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=8088, connection_limit=1000)
print('Successfully started run_receive_signals_server.')
print('Successfully started run_receive_signals_server.')
9 changes: 6 additions & 3 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 'quantity'
data = {
'trade_pair': TradePair.FTSE,
'trade_pair': TradePair.BTCUSD,
'order_type': OrderType.LONG,
'leverage': .05,
'leverage': 0.1, # leverage
# 'value': 10_000, # USD value
# 'quantity': 0.1, # base asset quantity (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)
10 changes: 5 additions & 5 deletions neurons/backtest_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def save_positions_to_manager(position_manager, hk_to_positions):
class BacktestManager:

def __init__(self, positions_at_t_f, start_time_ms, secrets, scoring_func,
capital=ValiConfig.DEFAULT_CAPITAL, use_slippage=None,
use_slippage=None,
fetch_slippage_data=False, recalculate_slippage=False, rebuild_all_positions=False,
parallel_mode: ParallelizationMode=ParallelizationMode.PYSPARK, build_portfolio_ledgers_only=False,
pool_size=0, target_ledger_window_ms=ValiConfig.TARGET_LEDGER_WINDOW_MS):
Expand Down Expand Up @@ -149,13 +149,13 @@ def __init__(self, positions_at_t_f, start_time_ms, secrets, scoring_func,
secrets=self.secrets,
use_slippage=use_slippage,
build_portfolio_ledgers_only=build_portfolio_ledgers_only,
target_ledger_window_ms=target_ledger_window_ms,
contract_manager=self.contract_manager)
target_ledger_window_ms=target_ledger_window_ms)


self.position_manager = PositionManager(metagraph=self.metagraph,
perf_ledger_manager=self.perf_ledger_manager,
elimination_manager=self.elimination_manager,
contract_manager=self.contract_manager,
is_backtesting=True,
challengeperiod_manager=None)

Expand Down Expand Up @@ -185,7 +185,7 @@ def __init__(self, positions_at_t_f, start_time_ms, secrets, scoring_func,
contract_manager=self.contract_manager,
)
self.psm = PriceSlippageModel(self.live_price_fetcher, is_backtesting=True, fetch_slippage_data=fetch_slippage_data,
recalculate_slippage=recalculate_slippage, capital=capital)
recalculate_slippage=recalculate_slippage)


#Until slippage is added to the db, this will always have to be done since positions are sometimes rebuilt and would require slippage attributes on orders and initial_entry_price calculation
Expand Down Expand Up @@ -364,7 +364,7 @@ def debug_print_ledgers(self, perf_ledger_bundles):
t0 = time.time()

secrets = ValiUtils.get_secrets() # {'polygon_apikey': '123', 'tiingo_apikey': '456'}
btm = BacktestManager(hk_to_positions, start_time_ms, secrets, None, capital=500_000,
btm = BacktestManager(hk_to_positions, start_time_ms, secrets, None,
use_slippage=use_slippage, fetch_slippage_data=False, recalculate_slippage=False,
parallel_mode=parallel_mode,
build_portfolio_ledgers_only=build_portfolio_ledgers_only)
Expand Down
73 changes: 58 additions & 15 deletions neurons/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,11 @@ def __init__(self):
signal_sync_condition=self.signal_sync_condition,
n_orders_being_processed=self.n_orders_being_processed,
ipc_manager=self.ipc_manager,
position_manager=None,
position_manager=None, # Set after self.pm creation
auto_sync_enabled=self.auto_sync,
contract_manager=self.contract_manager,
live_price_fetcher=self.live_price_fetcher,
asset_selection_manager=self.asset_selection_manager) # Set after self.pm creation
asset_selection_manager=self.asset_selection_manager)

self.p2p_syncer = P2PSyncer(wallet=self.wallet, metagraph=self.metagraph, is_testnet=not self.is_mainnet,
shutdown_dict=shutdown_dict, signal_sync_lock=self.signal_sync_lock,
Expand All @@ -224,16 +224,17 @@ def __init__(self):
self.perf_ledger_manager = PerfLedgerManager(self.metagraph, ipc_manager=self.ipc_manager,
shutdown_dict=shutdown_dict,
perf_ledger_hks_to_invalidate=self.position_syncer.perf_ledger_hks_to_invalidate,
position_manager=None,
contract_manager=self.contract_manager) # Set after self.pm creation)
position_manager=None) # Set after self.pm creation)


self.position_manager = PositionManager(metagraph=self.metagraph,
perform_order_corrections=True,
ipc_manager=self.ipc_manager,
live_price_fetcher=self.live_price_fetcher,
perf_ledger_manager=self.perf_ledger_manager,
elimination_manager=self.elimination_manager,
challengeperiod_manager=None,
contract_manager=self.contract_manager,
secrets=self.secrets,
shared_queue_websockets=self.shared_queue_websockets,
closed_position_daemon=True)
Expand Down Expand Up @@ -871,7 +872,7 @@ def _get_or_create_open_position_from_new_order(self, trade_pair: TradePair, ord
self._add_order_to_existing_position(existing_open_pos, trade_pair, OrderType.FLAT,
0.0, force_close_order_time, miner_hotkey,
price_sources, force_close_order_uuid, miner_repo_version,
OrderSource.MAX_ORDERS_PER_POSITION_CLOSE, account_size)
OrderSource.MAX_ORDERS_PER_POSITION_CLOSE)
time.sleep(0.1) # Put 100ms between two consecutive websocket writes for the same trade pair and hotkey. We need the new order to be seen after the FLAT.
else:
# If the position is closed, raise an exception. This can happen if the miner is eliminated in the main
Expand Down Expand Up @@ -1030,27 +1031,42 @@ def parse_miner_uuid(self, synapse: template.protocol.SendSignal):
return temp

def _add_order_to_existing_position(self, existing_position, trade_pair, signal_order_type: OrderType,
signal_leverage: float, order_time_ms: int, miner_hotkey: str,
quantity: float, order_time_ms: int, miner_hotkey: str,
price_sources, miner_order_uuid: str, miner_repo_version: str, src:OrderSource,
account_size):
usd_base_price=None):
# Must be locked by caller
best_price_source = price_sources[0]
price = best_price_source.parse_appropriate_price(order_time_ms, trade_pair.is_forex, signal_order_type, existing_position.orders[0].order_type)

if existing_position.account_size <= 0:
bt.logging.warning(
f"Invalid account_size {existing_position.account_size} for position {existing_position.position_uuid}. "
f"Using MIN_CAPITAL as fallback."
)
existing_position.account_size = ValiConfig.MIN_CAPITAL
# Calculate value and leverage
if usd_base_price is None:
usd_base_price = self.live_price_fetcher.get_usd_base_conversion(trade_pair, order_time_ms, price, signal_order_type, existing_position.position_type)
value = (1 / usd_base_price) * (quantity * trade_pair.lot_size)
leverage = value / existing_position.account_size
order = Order(
trade_pair=trade_pair,
order_type=signal_order_type,
leverage=signal_leverage,
price=best_price_source.parse_appropriate_price(order_time_ms, trade_pair.is_forex, signal_order_type,
existing_position),
quantity=quantity,
value=value,
leverage=leverage,
price=price,
processed_ms=order_time_ms,
order_uuid=miner_order_uuid,
price_sources=price_sources,
bid=best_price_source.bid,
ask=best_price_source.ask,
src=src
)
self.price_slippage_model.refresh_features_daily(time_ms=order_time_ms)
order.slippage = PriceSlippageModel.calculate_slippage(order.bid, order.ask, order, account_size)
order.usd_base_rate = usd_base_price
order.quote_usd_rate = self.live_price_fetcher.get_quote_usd_conversion(order, existing_position.position_type)
net_portfolio_leverage = self.position_manager.calculate_net_portfolio_leverage(miner_hotkey)
order.slippage = PriceSlippageModel.calculate_slippage(order.bid, order.ask, order)
existing_position.add_order(order, self.live_price_fetcher, net_portfolio_leverage)
self.position_manager.save_miner_position(existing_position)
# Update cooldown cache after successful order processing
Expand All @@ -1069,6 +1085,29 @@ def _get_account_size(self, miner_hotkey, now_ms):
account_size = max(account_size, ValiConfig.MIN_CAPITAL)
return account_size

@staticmethod
def parse_order_quantity(signal, usd_base_conversion, trade_pair, portfolio_value):
"""
parses an order signal and calculates leverage, value, and quantity
"""
leverage = signal.get("leverage")
value = signal.get("value")
quantity = signal.get("quantity")

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

if quantity is not None:
return quantity
if leverage is not None:
value = leverage * portfolio_value
quantity = (value * usd_base_conversion) / trade_pair.lot_size
elif value is not None:
quantity = (value * usd_base_conversion) / trade_pair.lot_size

return quantity

# 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 @@ -1102,7 +1141,6 @@ def receive_signal(self, synapse: template.protocol.SendSignal,
raise SignalException(
f"Ignoring order for [{miner_hotkey}] due to no live prices being found for trade_pair [{trade_pair}]. Please try again.")

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 @@ -1120,10 +1158,15 @@ def receive_signal(self, synapse: template.protocol.SendSignal,
existing_position = self._get_or_create_open_position_from_new_order(trade_pair, signal_order_type,
now_ms, miner_hotkey, miner_order_uuid, now_ms, price_sources, miner_repo_version, account_size)
if existing_position:
best_price_source = price_sources[0]
price = best_price_source.parse_appropriate_price(now_ms, trade_pair.is_forex, signal_order_type, existing_position.orders[0].order_type)
usd_base_price = self.live_price_fetcher.get_usd_base_conversion(trade_pair, now_ms, price, signal_order_type, existing_position.position_type)
quantity = self.parse_order_quantity(signal, usd_base_price, trade_pair, existing_position.account_size)

self._add_order_to_existing_position(existing_position, trade_pair, signal_order_type,
signal_leverage, now_ms, miner_hotkey,
quantity, now_ms, miner_hotkey,
price_sources, miner_order_uuid, miner_repo_version,
OrderSource.ORGANIC, account_size)
OrderSource.ORGANIC, usd_base_price)
synapse.order_json = existing_position.orders[-1].__str__()
else:
# Happens if a FLAT is sent when no position exists
Expand Down
67 changes: 63 additions & 4 deletions ptn_api/rest_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,22 +771,73 @@ def deposit_collateral():
bt.logging.error(f"Error processing collateral deposit: {e}")
return jsonify({'error': 'Internal server error processing deposit'}), 500

@self.app.route("/collateral/query-withdraw", methods=["POST"])
def query_withdraw_collateral():
"""Query collateral withdrawal request for potential slashed amount"""
# Check if contract manager is available
if not self.contract_manager:
return jsonify({'error': 'Collateral operations not available'}), 503

try:
# Parse JSON request
if not request.is_json:
return jsonify({'error': 'Content-Type must be application/json'}), 400

data = request.get_json()
if not data:
return jsonify({'error': 'Invalid JSON body'}), 400

# Validate required fields for withdrawal query
required_fields = ['amount', 'miner_hotkey']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Missing required field: {field}'}), 400

# Validate amount is a positive number
try:
amount = float(data['amount'])
if amount <= 0:
return jsonify({'error': 'Amount must be a positive number'}), 400
except (ValueError, TypeError):
return jsonify({'error': 'Amount must be a valid number'}), 400

# Validate miner_hotkey is a valid SS58 address
miner_hotkey = data['miner_hotkey']
try:
# Attempt to create a Keypair to validate SS58 format
Keypair(ss58_address=miner_hotkey)
except Exception:
return jsonify({'error': 'Invalid SS58 address format for miner_hotkey'}), 400

# Process the withdrawal query
result = self.contract_manager.query_withdrawal_request(
amount=amount,
miner_hotkey=miner_hotkey
)

# Return response
return jsonify(result)

except Exception as e:
bt.logging.error(f"Error processing collateral withdrawal query: {e}")
return jsonify({'error': 'Internal server error processing withdrawal query'}), 500

@self.app.route("/collateral/withdraw", methods=["POST"])
def withdraw_collateral():
"""Process collateral withdrawal request."""
# Check if contract manager is available
if not self.contract_manager:
return jsonify({'error': 'Collateral operations not available'}), 503

try:
# Parse JSON request
if not request.is_json:
return jsonify({'error': 'Content-Type must be application/json'}), 400

data = request.get_json()
if not data:
return jsonify({'error': 'Invalid JSON body'}), 400

# Validate required fields for signed withdrawal
required_fields = ['amount', 'miner_coldkey', 'miner_hotkey', 'nonce', 'timestamp', 'signature']
for field in required_fields:
Expand Down Expand Up @@ -821,6 +872,14 @@ def withdraw_collateral():
if not is_valid:
return jsonify({'error': f'{error_msg}'}), 401

# Validate amount is a positive number
try:
amount = float(data['amount'])
if amount <= 0:
return jsonify({'error': 'Amount must be a positive number'}), 400
except (ValueError, TypeError):
return jsonify({'error': 'Amount must be a valid number'}), 400

# Process the withdrawal using verified data
result = self.contract_manager.process_withdrawal_request(
amount=data['amount'],
Expand All @@ -830,7 +889,7 @@ def withdraw_collateral():

# Return response
return jsonify(result)

except Exception as e:
bt.logging.error(f"Error processing collateral withdrawal: {e}")
return jsonify({'error': 'Internal server error processing withdrawal'}), 500
Expand Down
Loading
Loading