|
3 | 3 | import os
|
4 | 4 | from dataclasses import dataclass
|
5 | 5 | from pathlib import Path
|
6 |
| -from typing import Dict, List |
| 6 | +from typing import Annotated, Dict, List |
7 | 7 |
|
8 | 8 | import click
|
9 | 9 | import numpy as np
|
|
16 | 16 | from pydantic import BaseModel
|
17 | 17 | from pydantic_ai import Agent, RunContext
|
18 | 18 | from silverback import SilverbackBot, StateSnapshot
|
| 19 | +from taskiq import Context, TaskiqDepends, TaskiqState |
19 | 20 |
|
20 | 21 | # Initialize bot
|
21 | 22 | bot = SilverbackBot()
|
@@ -230,11 +231,11 @@ def get_token_name(address: str) -> str:
|
230 | 231 | raise
|
231 | 232 |
|
232 | 233 |
|
233 |
| -@trading_agent.tool_plain(retries=3) |
234 |
| -def get_eligible_buy_tokens(ctx: AgentDependencies) -> List[str]: |
| 234 | +@trading_agent.tool(retries=3) |
| 235 | +def get_eligible_buy_tokens(ctx: RunContext[AgentDependencies]) -> List[str]: |
235 | 236 | """Return a list of tokens eligible for purchase (excluding the sell token)."""
|
236 | 237 | try:
|
237 |
| - sell_token = ctx.sell_token |
| 238 | + sell_token = ctx.deps.sell_token |
238 | 239 | return [token for token in MONITORED_TOKENS if token != sell_token]
|
239 | 240 | except Exception as e:
|
240 | 241 | print(f"[get_eligible_buy_tokens] failed with error: {e}")
|
@@ -304,11 +305,10 @@ def _get_token_balances() -> Dict[str, int]:
|
304 | 305 | return {token_address: balance for token_address, balance in zip(MONITORED_TOKENS, results)}
|
305 | 306 |
|
306 | 307 |
|
307 |
| -def _create_trade_context(lookback_blocks: int = 15000) -> TradeContext: |
| 308 | +def _create_trade_context( |
| 309 | + trades_df: pd.DataFrame, decisions_df: pd.DataFrame, lookback_blocks: int = 15000 |
| 310 | +) -> TradeContext: |
308 | 311 | """Create TradeContext with all required data"""
|
309 |
| - trades_df = _load_trades_db() |
310 |
| - decisions_df = _load_decisions_db() |
311 |
| - |
312 | 312 | prior_decisions = decisions_df.tail(3).copy()
|
313 | 313 | prior_decisions["metrics_snapshot"] = prior_decisions["metrics_snapshot"].apply(json.loads)
|
314 | 314 |
|
@@ -383,32 +383,36 @@ def _save_decision(decision: AgentDecision) -> pd.DataFrame:
|
383 | 383 | return decisions_df
|
384 | 384 |
|
385 | 385 |
|
386 |
| -def _update_latest_decision_outcome(final_price: float) -> pd.DataFrame: |
| 386 | +def _update_latest_decision_outcome( |
| 387 | + decisions_df: pd.DataFrame, final_price: float | None = None |
| 388 | +) -> pd.DataFrame: |
387 | 389 | """Update most recent decision with outcome data"""
|
388 |
| - decisions_df = _load_decisions_db() |
389 |
| - |
390 | 390 | if decisions_df.empty:
|
391 | 391 | return decisions_df
|
392 | 392 |
|
393 | 393 | latest_idx = decisions_df.index[-1]
|
394 | 394 | latest_decision = decisions_df.iloc[-1]
|
395 | 395 |
|
396 | 396 | if latest_decision.should_trade:
|
397 |
| - metrics = json.loads(latest_decision.metrics_snapshot) |
398 |
| - initial_price = next( |
399 |
| - m["last_price"] |
400 |
| - for m in metrics |
401 |
| - if m["token_a"] == latest_decision.sell_token |
402 |
| - and m["token_b"] == latest_decision.buy_token |
403 |
| - ) |
| 397 | + if final_price is None: |
| 398 | + return decisions_df |
| 399 | + else: |
| 400 | + metrics = json.loads(latest_decision.metrics_snapshot) |
| 401 | + initial_price = next( |
| 402 | + m["last_price"] |
| 403 | + for m in metrics |
| 404 | + if m["token_a"] == latest_decision.sell_token |
| 405 | + and m["token_b"] == latest_decision.buy_token |
| 406 | + ) |
404 | 407 |
|
405 |
| - profitable = ( |
406 |
| - final_price > initial_price |
407 |
| - if latest_decision.sell_token == metrics[0]["token_a"] |
408 |
| - else final_price < initial_price |
409 |
| - ) |
| 408 | + profitable = ( |
| 409 | + final_price > initial_price |
| 410 | + if latest_decision.sell_token == metrics[0]["token_a"] |
| 411 | + else final_price < initial_price |
| 412 | + ) |
| 413 | + |
| 414 | + decisions_df.loc[latest_idx, "profitable"] = int(profitable) |
410 | 415 |
|
411 |
| - decisions_df.loc[latest_idx, "profitable"] = profitable |
412 | 416 | _save_decisions_db(decisions_df)
|
413 | 417 |
|
414 | 418 | return decisions_df
|
@@ -452,20 +456,20 @@ def _save_trades_db(trades_dict: Dict) -> None:
|
452 | 456 | df.to_csv(TRADE_FILEPATH, index=False)
|
453 | 457 |
|
454 | 458 |
|
455 |
| -def _load_block_db() -> Dict: |
| 459 | +def _load_block_db() -> int: |
456 | 460 | """Load the last processed block from CSV file or create new if doesn't exist"""
|
457 | 461 | df = (
|
458 | 462 | pd.read_csv(BLOCK_FILEPATH)
|
459 | 463 | if os.path.exists(BLOCK_FILEPATH)
|
460 | 464 | else pd.DataFrame({"last_processed_block": [START_BLOCK]})
|
461 | 465 | )
|
462 |
| - return {"last_processed_block": df["last_processed_block"].iloc[0]} |
| 466 | + return df["last_processed_block"].iloc[0] |
463 | 467 |
|
464 | 468 |
|
465 |
| -def _save_block_db(data: Dict): |
| 469 | +def _save_block_db(block_number: int) -> None: |
466 | 470 | """Save the last processed block to CSV file"""
|
467 | 471 | os.makedirs(os.path.dirname(BLOCK_FILEPATH), exist_ok=True)
|
468 |
| - df = pd.DataFrame([data]) |
| 472 | + df = pd.DataFrame({"last_processed_block": [block_number]}) |
469 | 473 | df.to_csv(BLOCK_FILEPATH, index=False)
|
470 | 474 |
|
471 | 475 |
|
@@ -783,95 +787,183 @@ def bot_startup(startup_state: StateSnapshot):
|
783 | 787 | if PROMPT_AUTOSIGN and click.confirm("Enable autosign?"):
|
784 | 788 | bot.signer.set_autosign(enabled=True)
|
785 | 789 |
|
| 790 | + # Process historical trades |
786 | 791 | block_db = _load_block_db()
|
787 |
| - last_processed_block = block_db["last_processed_block"] |
788 |
| - _save_block_db({"last_processed_block": chain.blocks.head.number}) |
789 |
| - |
| 792 | + last_processed_block = block_db |
| 793 | + _save_block_db(chain.blocks.head.number) |
790 | 794 | _process_historical_trades(
|
791 | 795 | GPV2_SETTLEMENT_CONTRACT,
|
792 | 796 | start_block=last_processed_block,
|
793 | 797 | stop_block=chain.blocks.head.number,
|
794 | 798 | )
|
795 | 799 |
|
796 |
| - bot.state.agent = trading_agent |
| 800 | + # Initialize bot state |
797 | 801 |
|
798 | 802 | decisions_df = _load_decisions_db()
|
799 | 803 | if decisions_df.empty:
|
800 | 804 | bot.state.next_decision_block = chain.blocks.head.number
|
801 | 805 | else:
|
802 | 806 | bot.state.next_decision_block = decisions_df.iloc[-1].block_number + TRADING_BLOCK_COOLDOWN
|
803 | 807 |
|
| 808 | + bot.state.can_trade = False |
| 809 | + bot.state.sell_token = None |
| 810 | + |
804 | 811 | return {"message": "Starting...", "block_number": startup_state.last_block_seen}
|
805 | 812 |
|
806 | 813 |
|
807 |
| -@bot.on_(chain.blocks) |
808 |
| -def exec_block(block: BlockAPI): |
809 |
| - """Execute block handler with structured decision flow""" |
810 |
| - _save_block_db({"last_processed_block": block.number}) |
| 814 | +@bot.on_worker_startup() |
| 815 | +def worker_startup(state: TaskiqState): |
| 816 | + """Initialize worker state""" |
| 817 | + state.agent = trading_agent |
| 818 | + state.trades_df = _load_trades_db() |
| 819 | + state.decisions_df = _load_decisions_db() |
811 | 820 |
|
812 |
| - if (bot.state.next_decision_block - block.number) <= 5: |
813 |
| - _catch_up_trades( |
814 |
| - current_block=block.number, next_decision_block=bot.state.next_decision_block |
815 |
| - ) |
816 | 821 |
|
817 |
| - decisions_df = _load_decisions_db() |
| 822 | +@bot.on_(chain.blocks) |
| 823 | +def update_state(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]): |
| 824 | + """Update trade history and decision outcomes""" |
| 825 | + click.echo(f"\n[{block.number}] Starting state update...") |
| 826 | + _save_block_db(block.number) |
| 827 | + bot.state.can_trade = False |
| 828 | + click.echo(f"[{block.number}] State: trade={bot.state.can_trade}, sell={bot.state.sell_token}") |
| 829 | + |
| 830 | + if block.number < bot.state.next_decision_block: |
| 831 | + click.echo(f"[{block.number}] Skip - next decision at {bot.state.next_decision_block}") |
| 832 | + return {"message": "Skipped - before cooldown", "block": block.number} |
| 833 | + |
| 834 | + click.echo(f"[{block.number}] Past cooldown, catching up trades...") |
| 835 | + _catch_up_trades(current_block=block.number, next_decision_block=bot.state.next_decision_block) |
| 836 | + |
| 837 | + bot.state.sell_token = _select_sell_token() |
| 838 | + click.echo(f"[{block.number}] Sell token: {bot.state.sell_token}") |
| 839 | + |
| 840 | + if not bot.state.sell_token: |
| 841 | + click.echo(f"[{block.number}] No eligible sell tokens found") |
| 842 | + return {"message": "No eligible sell tokens", "block": block.number} |
| 843 | + |
| 844 | + if context.state.decisions_df.empty: |
| 845 | + click.echo(f"[{block.number}] No previous decisions, enabling trading") |
| 846 | + bot.state.can_trade = True |
| 847 | + return {"message": "No previous decisions", "can_trade": True} |
| 848 | + |
| 849 | + latest_decision = context.state.decisions_df.iloc[-1] |
| 850 | + msg = ( |
| 851 | + f"[{block.number}] Latest: " |
| 852 | + f"trade={latest_decision.should_trade}, " |
| 853 | + f"block={latest_decision.block_number}" |
| 854 | + ) |
| 855 | + click.echo(msg) |
818 | 856 |
|
819 |
| - if decisions_df.empty: |
820 |
| - should_trade = True |
821 |
| - latest_decision = None |
822 |
| - else: |
823 |
| - latest_decision = decisions_df.iloc[-1] |
824 |
| - should_trade = block.number - latest_decision.block_number >= TRADING_BLOCK_COOLDOWN |
| 857 | + if not latest_decision.should_trade: |
| 858 | + click.echo(f"[{block.number}] Last decision wasn't a trade, enabling trading") |
| 859 | + bot.state.can_trade = True |
| 860 | + return { |
| 861 | + "message": "Last decision was not a trade", |
| 862 | + "can_trade": True, |
| 863 | + "last_decision_block": latest_decision.block_number, |
| 864 | + } |
825 | 865 |
|
826 |
| - if not should_trade: |
827 |
| - return |
| 866 | + click.echo(f"[{block.number}] Creating trade context for outcome update...") |
| 867 | + trade_ctx = _create_trade_context( |
| 868 | + trades_df=context.state.trades_df, decisions_df=context.state.decisions_df |
| 869 | + ) |
828 | 870 |
|
829 |
| - trade_ctx = _create_trade_context() |
| 871 | + matching_metrics = [ |
| 872 | + m.last_price |
| 873 | + for m in trade_ctx.metrics |
| 874 | + if m.token_a == latest_decision.sell_token and m.token_b == latest_decision.buy_token |
| 875 | + ] |
830 | 876 |
|
831 |
| - if latest_decision is not None and latest_decision.should_trade: |
832 |
| - _update_latest_decision_outcome( |
833 |
| - final_price=next( |
834 |
| - m.last_price |
835 |
| - for m in trade_ctx.metrics |
836 |
| - if m.token_a == latest_decision.sell_token |
837 |
| - and m.token_b == latest_decision.buy_token |
838 |
| - ) |
| 877 | + if not matching_metrics: |
| 878 | + click.echo( |
| 879 | + f"[{block.number}] No metrics {latest_decision.sell_token}-{latest_decision.buy_token}" |
839 | 880 | )
|
| 881 | + context.state.decisions_df = _update_latest_decision_outcome( |
| 882 | + decisions_df=context.state.decisions_df, |
| 883 | + final_price=None, |
| 884 | + ) |
| 885 | + bot.state.can_trade = True |
| 886 | + return { |
| 887 | + "message": "Marked as unknown outcome", |
| 888 | + "block": block.number, |
| 889 | + "can_trade": True, |
| 890 | + } |
| 891 | + |
| 892 | + click.echo(f"[{block.number}] Updating previous decision outcome...") |
| 893 | + context.state.decisions_df = _update_latest_decision_outcome( |
| 894 | + decisions_df=context.state.decisions_df, final_price=matching_metrics[0] |
| 895 | + ) |
| 896 | + |
| 897 | + bot.state.can_trade = True |
| 898 | + click.echo(f"[{block.number}] State: trade={bot.state.can_trade}, sell={bot.state.sell_token}") |
| 899 | + return { |
| 900 | + "message": "Updated previous decision outcome", |
| 901 | + "can_trade": True, |
| 902 | + "last_decision_block": latest_decision.block_number, |
| 903 | + } |
| 904 | + |
840 | 905 |
|
841 |
| - sell_token = _select_sell_token() |
842 |
| - deps = AgentDependencies(trade_ctx=trade_ctx, sell_token=sell_token) |
| 906 | +@bot.on_(chain.blocks) |
| 907 | +def make_trading_decision(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]): |
| 908 | + """Make and execute trading decisions""" |
| 909 | + click.echo(f"\n[{block.number}] Starting trading decision...") |
| 910 | + click.echo(f"[{block.number}] State: trade={bot.state.can_trade}, sell={bot.state.sell_token}") |
| 911 | + |
| 912 | + if not bot.state.can_trade: |
| 913 | + click.echo(f"[{block.number}] Trading not enabled, skipping") |
| 914 | + return {"message": "Trading not enabled", "block": block.number} |
| 915 | + |
| 916 | + click.echo(f"[{block.number}] Creating trade context...") |
| 917 | + trade_ctx = _create_trade_context( |
| 918 | + trades_df=context.state.trades_df, decisions_df=context.state.decisions_df |
| 919 | + ) |
| 920 | + |
| 921 | + click.echo(f"[{block.number}] Running agent with sell_token={bot.state.sell_token}...") |
| 922 | + deps = AgentDependencies(trade_ctx=trade_ctx, sell_token=bot.state.sell_token) |
843 | 923 |
|
844 | 924 | loop = asyncio.new_event_loop()
|
845 | 925 | asyncio.set_event_loop(loop)
|
846 | 926 |
|
847 |
| - try: |
848 |
| - result = bot.state.agent.run_sync( |
849 |
| - "Analyze current market conditions and make a trading decision", deps=deps |
850 |
| - ) |
851 |
| - except Exception as e: |
852 |
| - click.echo(f"Anthropic API error at block {block.number}: {str(e)}") |
853 |
| - return |
| 927 | + result = context.state.agent.run_sync( |
| 928 | + "Analyze current market conditions and make a trading decision", deps=deps |
| 929 | + ) |
854 | 930 |
|
| 931 | + click.echo( |
| 932 | + f"[{block.number}] Agent: trade={result.data.should_trade}, buy={result.data.buy_token}" |
| 933 | + ) |
855 | 934 | _save_reasoning(block.number, result.data.reasoning)
|
856 | 935 |
|
857 | 936 | decision = _build_decision(
|
858 | 937 | block_number=block.number,
|
859 | 938 | response=result.data,
|
860 | 939 | metrics=trade_ctx.metrics,
|
861 |
| - sell_token=sell_token, |
| 940 | + sell_token=bot.state.sell_token, |
862 | 941 | )
|
863 | 942 |
|
864 | 943 | decision.valid = _validate_decision(decision)
|
| 944 | + click.echo(f"[{block.number}] Decision valid={decision.valid}") |
865 | 945 | _save_decision(decision)
|
866 |
| - bot.state.next_decision_block = block.number + TRADING_BLOCK_COOLDOWN |
867 | 946 |
|
868 |
| - if decision.valid: |
| 947 | + if decision.valid and decision.should_trade: |
| 948 | + click.echo(f"[{block.number}] Order: {decision.sell_token} -> {decision.buy_token}") |
869 | 949 | order_uid, error = create_submit_and_sign_order(
|
870 | 950 | sell_token=decision.sell_token,
|
871 | 951 | buy_token=decision.buy_token,
|
872 | 952 | sell_amount=trade_ctx.token_balances[decision.sell_token],
|
873 | 953 | )
|
874 | 954 | if error:
|
875 |
| - click.echo(f"Order failed: {error}") |
| 955 | + click.echo(f"[{block.number}] Order failed: {error}") |
876 | 956 | else:
|
877 |
| - click.echo(f"Order submitted successfully. UID: {order_uid}") |
| 957 | + click.echo(f"[{block.number}] Order: {order_uid}") |
| 958 | + |
| 959 | + bot.state.next_decision_block = block.number + TRADING_BLOCK_COOLDOWN |
| 960 | + click.echo(f"[{block.number}] Next decision: {bot.state.next_decision_block}") |
| 961 | + |
| 962 | + return { |
| 963 | + "message": "Trading decision made", |
| 964 | + "block": block.number, |
| 965 | + "should_trade": decision.should_trade, |
| 966 | + "sell_token": decision.sell_token, |
| 967 | + "buy_token": decision.buy_token, |
| 968 | + "next_decision_block": bot.state.next_decision_block, |
| 969 | + } |
0 commit comments