Skip to content

Commit e1e7253

Browse files
v14.3.0 (#47)
1 parent 4fc8b8c commit e1e7253

File tree

5 files changed

+286
-3
lines changed

5 files changed

+286
-3
lines changed

.coverage

4 KB
Binary file not shown.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,11 @@ config = {
889889
- Returns: `making_amount`, `taking_amount` (use these for privy_trigger)
890890
- `price_change_percentage`: Use "-0.5" for 0.5% lower (buy the dip), "10" for 10% higher (sell high)
891891

892+
- `limit_order_info` - Calculate trigger price and USD values for displaying order info
893+
- Params: `making_amount`, `taking_amount`, `input_price_usd`, `output_price_usd`
894+
- Returns: `making_usd`, `taking_usd_at_current`, `trigger_price_usd`, `current_output_price_usd`, `price_difference_percent`, `should_fill_now`
895+
- Use this when listing orders to show meaningful price info to users
896+
892897
- `to_smallest_units` - Convert human amount to smallest units
893898
- Params: `human_amount`, `decimals`
894899
- Returns: `smallest_units`

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "sakit"
3-
version = "14.2.2"
3+
version = "14.3.0"
44
description = "Solana Agent Kit"
55
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
66
license = "MIT"

sakit/token_math.py

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,68 @@ def calculate_limit_order_amounts(
213213
}
214214

215215

216+
def calculate_limit_order_info(
217+
making_amount: str,
218+
taking_amount: str,
219+
input_price_usd: str,
220+
output_price_usd: str,
221+
) -> Dict[str, str]:
222+
"""
223+
Calculate display info for a limit order from its amounts.
224+
225+
Use this when listing orders to show USD values and trigger prices.
226+
227+
Args:
228+
making_amount: Human-readable amount of input token being sold
229+
taking_amount: Human-readable amount of output token to receive
230+
input_price_usd: Current market price of input token in USD
231+
output_price_usd: Current market price of output token in USD
232+
233+
Returns:
234+
Dict with USD values and trigger price info
235+
"""
236+
try:
237+
making = Decimal(str(making_amount))
238+
taking = Decimal(str(taking_amount))
239+
input_price = Decimal(str(input_price_usd))
240+
output_price = Decimal(str(output_price_usd))
241+
242+
# Calculate USD values at current prices
243+
making_usd = making * input_price
244+
taking_usd_at_current = taking * output_price
245+
246+
# Calculate the trigger price (price per output token the order expects)
247+
# trigger_price = making_usd / taking_amount
248+
if taking > 0:
249+
trigger_price = making_usd / taking
250+
else:
251+
trigger_price = Decimal("0")
252+
253+
# Calculate price difference from current market
254+
if output_price > 0:
255+
price_diff_pct = ((trigger_price - output_price) / output_price) * 100
256+
else:
257+
price_diff_pct = Decimal("0")
258+
259+
# Determine if order should fill (for buy orders, trigger >= current means fill)
260+
# For a buy order: you want to buy when price drops TO trigger_price
261+
# Order fills when current_price <= trigger_price
262+
will_fill = output_price <= trigger_price
263+
264+
return {
265+
"making_amount": str(making),
266+
"taking_amount": str(taking),
267+
"making_usd": str(making_usd),
268+
"taking_usd_at_current": str(taking_usd_at_current),
269+
"trigger_price_usd": str(trigger_price),
270+
"current_output_price_usd": str(output_price),
271+
"price_difference_percent": str(price_diff_pct),
272+
"should_fill_now": will_fill,
273+
}
274+
except (InvalidOperation, ValueError) as e:
275+
raise ValueError(f"Invalid limit order info calculation: {e}")
276+
277+
216278
class TokenMathTool(AutoTool):
217279
"""
218280
Reliable token math calculations for swaps, transfers, and limit orders.
@@ -229,6 +291,7 @@ def __init__(self, registry: Optional[ToolRegistry] = None):
229291
"Actions: 'swap' (for privy_ultra - returns smallest_units), "
230292
"'transfer' (for privy_transfer - returns human-readable amount), "
231293
"'limit_order' (for privy_trigger - returns making_amount and taking_amount), "
294+
"'limit_order_info' (for displaying order list - calculates trigger price and USD values), "
232295
"'to_smallest_units', 'to_human', 'usd_to_tokens'. "
233296
"ALWAYS use this tool for any calculation - never do math yourself!"
234297
),
@@ -245,6 +308,7 @@ def get_schema(self) -> Dict[str, Any]:
245308
"swap",
246309
"transfer",
247310
"limit_order",
311+
"limit_order_info",
248312
"to_smallest_units",
249313
"to_human",
250314
"usd_to_tokens",
@@ -254,6 +318,7 @@ def get_schema(self) -> Dict[str, Any]:
254318
"'swap' - For privy_ultra: calculate smallest_units from USD amount, "
255319
"'transfer' - For privy_transfer: calculate human-readable token amount from USD, "
256320
"'limit_order' - For privy_trigger: calculate making_amount and taking_amount, "
321+
"'limit_order_info' - For displaying orders: calculate trigger price and USD values from order amounts, "
257322
"'to_smallest_units' - Convert human amount to smallest units, "
258323
"'to_human' - Convert smallest units to human readable, "
259324
"'usd_to_tokens' - Calculate token amount from USD value"
@@ -289,7 +354,7 @@ def get_schema(self) -> Dict[str, Any]:
289354
},
290355
"output_price_usd": {
291356
"type": "string",
292-
"description": "Output token price in USD (for 'limit_order'). Get from Birdeye. Pass empty string if not needed.",
357+
"description": "Output token price in USD (for 'limit_order', 'limit_order_info'). Get from Birdeye. Pass empty string if not needed.",
293358
},
294359
"output_decimals": {
295360
"type": "integer",
@@ -299,6 +364,14 @@ def get_schema(self) -> Dict[str, Any]:
299364
"type": "string",
300365
"description": "Price change percentage for limit order (for 'limit_order'). E.g., '-0.5' for 0.5% lower (buy dip), '10' for 10% higher (sell high). Pass '0' for current price.",
301366
},
367+
"making_amount": {
368+
"type": "string",
369+
"description": "Human-readable amount of input token being sold (for 'limit_order_info'). From order's makingAmount field. Pass empty string if not needed.",
370+
},
371+
"taking_amount": {
372+
"type": "string",
373+
"description": "Human-readable amount of output token to receive (for 'limit_order_info'). From order's takingAmount field. Pass empty string if not needed.",
374+
},
302375
},
303376
"required": [
304377
"action",
@@ -312,6 +385,8 @@ def get_schema(self) -> Dict[str, Any]:
312385
"output_price_usd",
313386
"output_decimals",
314387
"price_change_percentage",
388+
"making_amount",
389+
"taking_amount",
315390
],
316391
"additionalProperties": False,
317392
}
@@ -333,6 +408,8 @@ async def execute(
333408
output_price_usd: str = "",
334409
output_decimals: int = 0,
335410
price_change_percentage: str = "0",
411+
making_amount: str = "",
412+
taking_amount: str = "",
336413
) -> Dict[str, Any]:
337414
"""Execute the math calculation."""
338415
action = action.lower().strip()
@@ -449,10 +526,41 @@ async def execute(
449526
"message": f"${usd_amount} at price ${token_price_usd} = {result} tokens",
450527
}
451528

529+
elif action == "limit_order_info":
530+
# Calculate display info for a limit order from its amounts
531+
if not all(
532+
[making_amount, taking_amount, input_price_usd, output_price_usd]
533+
):
534+
return {
535+
"status": "error",
536+
"message": "Missing required params for 'limit_order_info': making_amount, taking_amount, input_price_usd, output_price_usd",
537+
}
538+
result = calculate_limit_order_info(
539+
making_amount=making_amount,
540+
taking_amount=taking_amount,
541+
input_price_usd=input_price_usd,
542+
output_price_usd=output_price_usd,
543+
)
544+
return {
545+
"status": "success",
546+
"action": "limit_order_info",
547+
"making_amount": result["making_amount"],
548+
"taking_amount": result["taking_amount"],
549+
"making_usd": result["making_usd"],
550+
"taking_usd_at_current": result["taking_usd_at_current"],
551+
"trigger_price_usd": result["trigger_price_usd"],
552+
"current_output_price_usd": result["current_output_price_usd"],
553+
"price_difference_percent": result["price_difference_percent"],
554+
"should_fill_now": result["should_fill_now"],
555+
"message": f"Order: sell {result['making_amount']} (${result['making_usd']}) for {result['taking_amount']} tokens. "
556+
f"Trigger price: ${result['trigger_price_usd']}, Current price: ${result['current_output_price_usd']} "
557+
f"({result['price_difference_percent']}% diff). Will fill now: {result['should_fill_now']}",
558+
}
559+
452560
else:
453561
return {
454562
"status": "error",
455-
"message": f"Unknown action: {action}. Valid: swap, limit_order, to_smallest_units, to_human, usd_to_tokens",
563+
"message": f"Unknown action: {action}. Valid: swap, transfer, limit_order, limit_order_info, to_smallest_units, to_human, usd_to_tokens",
456564
}
457565

458566
except ValueError as e:

tests/test_token_math_tool.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
apply_percentage_change,
2020
calculate_swap_amount,
2121
calculate_limit_order_amounts,
22+
calculate_limit_order_info,
2223
)
2324

2425

@@ -279,6 +280,104 @@ def test_limit_order_sell_high(self):
279280
assert taking < 71428571 # Less than at current price
280281

281282

283+
class TestCalculateLimitOrderInfo:
284+
"""Test calculate_limit_order_info function."""
285+
286+
def test_order_at_current_price(self):
287+
"""Should calculate info for order at current market price."""
288+
# Order: sell 0.036 SOL for 521714 BONK
289+
result = calculate_limit_order_info(
290+
making_amount="0.036001982",
291+
taking_amount="521714.66282",
292+
input_price_usd="139", # Current SOL price
293+
output_price_usd="0.0000096", # Current BONK price
294+
)
295+
# making_usd = 0.036 * 139 = ~$5
296+
assert float(result["making_usd"]) == pytest.approx(5.00, rel=0.01)
297+
# trigger_price = making_usd / taking_amount = $5 / 521714 = ~$0.00000958
298+
trigger_price = float(result["trigger_price_usd"])
299+
assert trigger_price == pytest.approx(0.00000958, rel=0.01)
300+
# Current price is $0.0000096, trigger is ~$0.00000958
301+
# Order wants to buy at LOWER price, so should NOT fill now
302+
assert result["should_fill_now"] is False
303+
304+
def test_order_should_fill(self):
305+
"""Should detect when order should fill (price dropped to trigger)."""
306+
result = calculate_limit_order_info(
307+
making_amount="0.036",
308+
taking_amount="521714",
309+
input_price_usd="139", # SOL price
310+
output_price_usd="0.0000090", # BONK price dropped below trigger
311+
)
312+
# trigger_price = $5 / 521714 = ~$0.00000958
313+
# Current price = $0.0000090 which is BELOW trigger
314+
# Order should fill now!
315+
assert result["should_fill_now"] is True
316+
317+
def test_price_difference_calculation(self):
318+
"""Should calculate price difference correctly."""
319+
result = calculate_limit_order_info(
320+
making_amount="0.07", # ~$10 at $140/SOL
321+
taking_amount="1000000", # 1M BONK
322+
input_price_usd="140",
323+
output_price_usd="0.00001", # Current BONK price
324+
)
325+
# trigger_price = (0.07 * 140) / 1000000 = $0.0000098 per BONK
326+
trigger_price = float(result["trigger_price_usd"])
327+
assert trigger_price == pytest.approx(0.0000098, rel=0.01)
328+
# price_diff = (trigger - current) / current * 100
329+
# = (0.0000098 - 0.00001) / 0.00001 * 100 = -2%
330+
price_diff = float(result["price_difference_percent"])
331+
assert price_diff == pytest.approx(-2.0, rel=0.1)
332+
333+
def test_usd_values(self):
334+
"""Should calculate USD values correctly."""
335+
result = calculate_limit_order_info(
336+
making_amount="1", # 1 SOL
337+
taking_amount="10000", # 10,000 USDC
338+
input_price_usd="140",
339+
output_price_usd="1", # USDC
340+
)
341+
# making_usd = 1 * 140 = $140
342+
assert result["making_usd"] == "140"
343+
# taking_usd_at_current = 10000 * 1 = $10,000
344+
assert result["taking_usd_at_current"] == "10000"
345+
# trigger_price = $140 / 10000 = $0.014 per USDC
346+
assert float(result["trigger_price_usd"]) == pytest.approx(0.014, rel=0.001)
347+
348+
def test_zero_taking_amount(self):
349+
"""Should handle zero taking amount (edge case)."""
350+
result = calculate_limit_order_info(
351+
making_amount="1",
352+
taking_amount="0", # Zero output
353+
input_price_usd="140",
354+
output_price_usd="1",
355+
)
356+
# trigger_price should be 0 when taking is 0
357+
assert result["trigger_price_usd"] == "0"
358+
359+
def test_zero_output_price(self):
360+
"""Should handle zero output price (edge case)."""
361+
result = calculate_limit_order_info(
362+
making_amount="1",
363+
taking_amount="1000",
364+
input_price_usd="140",
365+
output_price_usd="0", # Zero price
366+
)
367+
# price_diff should be 0 when output_price is 0
368+
assert result["price_difference_percent"] == "0"
369+
370+
def test_invalid_amounts(self):
371+
"""Should raise error for invalid amounts."""
372+
with pytest.raises(ValueError):
373+
calculate_limit_order_info(
374+
making_amount="invalid",
375+
taking_amount="100",
376+
input_price_usd="140",
377+
output_price_usd="1",
378+
)
379+
380+
282381
# =============================================================================
283382
# Integration Tests for Tool
284383
# =============================================================================
@@ -486,6 +585,77 @@ async def test_limit_order_missing_params(self, math_tool):
486585
assert result["status"] == "error"
487586

488587

588+
class TestTokenMathToolExecuteLimitOrderInfo:
589+
"""Test 'limit_order_info' action."""
590+
591+
@pytest.mark.asyncio
592+
async def test_limit_order_info_success(self, math_tool):
593+
"""Should calculate order info correctly."""
594+
result = await math_tool.execute(
595+
action="limit_order_info",
596+
usd_amount="",
597+
token_price_usd="",
598+
decimals=0,
599+
human_amount="",
600+
smallest_units="",
601+
input_price_usd="139", # SOL current price
602+
input_decimals=0,
603+
output_price_usd="0.0000096", # BONK current price
604+
output_decimals=0,
605+
price_change_percentage="0",
606+
making_amount="0.036", # Selling 0.036 SOL
607+
taking_amount="521714", # Buying 521k BONK
608+
)
609+
assert result["status"] == "success"
610+
assert result["action"] == "limit_order_info"
611+
# Check USD values
612+
assert float(result["making_usd"]) == pytest.approx(5.0, rel=0.01)
613+
# Check trigger price
614+
assert "trigger_price_usd" in result
615+
assert "should_fill_now" in result
616+
617+
@pytest.mark.asyncio
618+
async def test_limit_order_info_should_fill(self, math_tool):
619+
"""Should detect when order should fill."""
620+
result = await math_tool.execute(
621+
action="limit_order_info",
622+
usd_amount="",
623+
token_price_usd="",
624+
decimals=0,
625+
human_amount="",
626+
smallest_units="",
627+
input_price_usd="139",
628+
input_decimals=0,
629+
output_price_usd="0.0000090", # Price dropped below trigger
630+
output_decimals=0,
631+
price_change_percentage="0",
632+
making_amount="0.036",
633+
taking_amount="521714",
634+
)
635+
assert result["status"] == "success"
636+
assert result["should_fill_now"] is True
637+
638+
@pytest.mark.asyncio
639+
async def test_limit_order_info_missing_params(self, math_tool):
640+
"""Should return error when params missing."""
641+
result = await math_tool.execute(
642+
action="limit_order_info",
643+
usd_amount="",
644+
token_price_usd="",
645+
decimals=0,
646+
human_amount="",
647+
smallest_units="",
648+
input_price_usd="", # Missing
649+
input_decimals=0,
650+
output_price_usd="", # Missing
651+
output_decimals=0,
652+
price_change_percentage="0",
653+
making_amount="", # Missing
654+
taking_amount="", # Missing
655+
)
656+
assert result["status"] == "error"
657+
658+
489659
class TestTokenMathToolExecuteToSmallestUnits:
490660
"""Test 'to_smallest_units' action."""
491661

0 commit comments

Comments
 (0)