diff --git a/floating_maker.py b/floating_maker.py index e869a35..18a4e9a 100644 --- a/floating_maker.py +++ b/floating_maker.py @@ -4,14 +4,14 @@ from anchorpy import Wallet from anchorpy import Provider -from solana.keypair import Keypair +from solders.keypair import Keypair from solana.rpc.async_api import AsyncClient from driftpy.constants.config import configs from driftpy.types import * #MarketType, OrderType, OrderParams, PositionDirection, OrderTriggerCondition -from driftpy.clearing_house import ClearingHouse +from driftpy.drift_client import DriftClient from driftpy.constants.numeric_constants import BASE_PRECISION, PRICE_PRECISION from borsh_construct.enum import _rust_enum @@ -55,7 +55,7 @@ async def main( wallet = Wallet(kp) connection = AsyncClient(url) provider = Provider(connection, wallet) - drift_acct = ClearingHouse.from_config(config, provider) + drift_acct = DriftClient.from_config(config, provider) is_perp = 'PERP' in market_name.upper() market_type = MarketType.PERP() if is_perp else MarketType.SPOT() diff --git a/if_stake.py b/if_stake.py index 4aac9ac..8687acd 100644 --- a/if_stake.py +++ b/if_stake.py @@ -6,15 +6,17 @@ from anchorpy import Provider from solana.rpc.async_api import AsyncClient -from solana.keypair import Keypair +from solders.keypair import Keypair from solana.rpc import commitment +from solana.transaction import AccountMeta, Instruction +from spl.token.constants import TOKEN_PROGRAM_ID import time from driftpy.constants.config import configs -from driftpy.clearing_house import ClearingHouse +from driftpy.drift_client import DriftClient from driftpy.accounts import * -from driftpy.clearing_house_user import ClearingHouseUser +from driftpy.drift_user import DriftUser async def view_logs( sig: str, @@ -56,8 +58,8 @@ async def main( provider = Provider(connection, wallet) from driftpy.constants.numeric_constants import QUOTE_PRECISION - ch = ClearingHouse.from_config(config, provider) - chu = ClearingHouseUser(ch) + ch = DriftClient.from_config(config, provider) + chu = DriftUser(ch) print(ch.program_id) from spl.token.instructions import get_associated_token_address, transfer, TransferParams @@ -84,11 +86,11 @@ async def main( if spot_market_index == 1: # send to WSOL and sync # https://github.dev/solana-labs/solana-program-library/token/js/src/ix/types.ts - keys = [AccountMeta(pubkey=ch.spot_market_atas[spot_market_index], is_signer=False, is_writable=True)] + accounts = [AccountMeta(pubkey=ch.spot_market_atas[spot_market_index], is_signer=False, is_writable=True)] data = int.to_bytes(17, 1, 'little') program_id = TOKEN_PROGRAM_ID - ix = TransactionInstruction( - keys=keys, + ix = Instruction( + accounts=accounts, program_id=program_id, data=data ) diff --git a/initialize_vault.py b/initialize_vault.py new file mode 100644 index 0000000..e69aea4 --- /dev/null +++ b/initialize_vault.py @@ -0,0 +1,136 @@ +import requests + +from solders.pubkey import Pubkey +from solders.keypair import Keypair +from anchorpy import Program, Wallet, Context, Idl, Provider + +import solders +from solana.transaction import Transaction +from solana.rpc.async_api import AsyncClient +import json +import os +from driftpy.constants.config import configs +from driftpy.drift_client import DriftClient +from driftpy.accounts import * +from spl.token.constants import TOKEN_PROGRAM_ID +from spl.token.instructions import get_associated_token_address + + +async def main(keypath, + env, + url, + name, + ): + with open(os.path.expanduser(keypath), 'r') as f: + secret = json.load(f) + kp = Keypair.from_secret_key(bytes(secret)) + print('using public key:', kp.public_key) + wallet = Wallet(kp) + connection = AsyncClient(url) + provider = Provider(connection, wallet) + + url = 'https://raw.githubusercontent.com/drift-labs/drift-vaults/master/ts/sdk/src/idl/drift_vaults.json' + response = requests.get(url) + data = response.json() + idl = data + pid = 'vAuLTsyrvSfZRuRB3XgvkPwNGgYSs9YRYymVebLKoxR' + vault_program = Program( + Idl.from_json(idl), + Pubkey(pid), + provider, + ) + config = configs[env] + drift_client = DriftClient.from_config(config, provider) + + # Initialize an empty list to store the character number array + char_number_array = [0] * 32 + + # Iterate through each character in the string and get its Unicode code point + for i in range(32): + if i < len(name): + char_number_array[i] = ord(name[i]) + + # Print the original string and the character number array + vault_pubkey = Pubkey.find_program_address( + [b"vault", bytes(char_number_array)], Pubkey(pid) + )[0] + params = { + 'name': char_number_array, + 'spot_market_index': 0, # USDC spot market index + 'redeem_period': int(60 * 60 * 24 * 30), # 30 days + 'max_tokens': int(1_000_000 * 1e6), + 'min_deposit_amount': int(100 * 1e6), + 'management_fee': int(.02 * 1e6), + 'profit_share': int(.2 * 1e6), + 'hurdle_rate': 0, # no supported atm + 'permissioned': False, + } + # ch_signer = get_clearing_house_signer_public_key(drift_client.program_id) + spot_market = await get_spot_market_account( + drift_client.program, params['spot_market_index'] + ) + + vault_user = get_user_account_public_key(drift_client.program_id, vault_pubkey) + vault_user_stats = get_user_stats_account_public_key(drift_client.program_id, vault_pubkey) + + # vault_ata = get_associated_token_address(drift_client.authority, spot_market.mint) + ata = Pubkey.find_program_address( + [b"vault_token_account", bytes(vault_pubkey)], vault_program.program_id + )[0] + + instruction = vault_program.instruction['initialize_vault']( + params, + ctx=Context( + accounts={ + 'drift_spot_market': spot_market.pubkey, + 'drift_spot_market_mint': spot_market.mint, + 'drift_user_stats': vault_user_stats, + 'drift_user': vault_user, + 'drift_state': drift_client.get_state_public_key(), + 'vault': vault_pubkey, + 'token_account': ata, + 'token_program': TOKEN_PROGRAM_ID, + 'drift_program': drift_client.program_id, + 'manager': drift_client.signer.public_key, + 'payer': drift_client.signer.public_key, + "rent": solders.sysvar.RENT, + "system_program": solders.system_program.ID, + }), + ) + + tx = Transaction() + tx.add(instruction) + txSig = await vault_program.provider.send(tx) + print(f"tx sig {txSig}") + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('--keypath', type=str, required=False, default=os.environ.get('ANCHOR_WALLET')) + parser.add_argument('--name', type=str, required=True, default='devnet') + parser.add_argument('--env', type=str, default='devnet') + args = parser.parse_args() + + if args.keypath is None: + if os.environ['ANCHOR_WALLET'] is None: + raise NotImplementedError("need to provide keypath or set ANCHOR_WALLET") + else: + args.keypath = os.environ['ANCHOR_WALLET'] + + if args.env == 'devnet': + url = 'https://api.devnet.solana.com' + elif args.env == 'mainnet': + url = 'https://api.mainnet-beta.solana.com' + else: + raise NotImplementedError('only devnet/mainnet env supported') + + import asyncio + + asyncio.run(main( + args.keypath, + args.env, + url, + args.name, + )) \ No newline at end of file diff --git a/limit_order_grid.py b/limit_order_grid.py index df89944..25d85f7 100644 --- a/limit_order_grid.py +++ b/limit_order_grid.py @@ -4,17 +4,16 @@ from anchorpy import Wallet from anchorpy import Provider -from solana.keypair import Keypair +from solders.keypair import Keypair from solana.rpc.async_api import AsyncClient from driftpy.constants.config import configs from driftpy.types import * #MarketType, OrderType, OrderParams, PositionDirection, OrderTriggerCondition from driftpy.accounts import get_perp_market_account, get_spot_market_account -from driftpy.math.oracle import get_oracle_data from driftpy.math.spot_market import get_signed_token_amount, get_token_amount -from driftpy.clearing_house import ClearingHouse -from driftpy.clearing_house_user import ClearingHouseUser +from driftpy.drift_client import DriftClient +from driftpy.drift_user import DriftUser from driftpy.constants.numeric_constants import BASE_PRECISION, PRICE_PRECISION from borsh_construct.enum import _rust_enum @@ -93,11 +92,11 @@ async def main( connection = AsyncClient(url) provider = Provider(connection, wallet) if authority: - authority_pubkey = PublicKey(authority) + authority_pubkey = Pubkey(authority) else: authority_pubkey = None - drift_acct = ClearingHouse.from_config(config, provider, authority=authority_pubkey) - chu = ClearingHouseUser(drift_acct) + drift_acct = DriftClient.from_config(config, provider, authority=authority_pubkey) + chu = DriftUser(drift_acct) is_perp = 'PERP' in market_name.upper() market_type = MarketType.PERP() if is_perp else MarketType.SPOT() @@ -117,7 +116,7 @@ async def main( market = await get_perp_market_account( drift_acct.program, market_index ) - oracle_data = await get_oracle_data(connection, market.amm.oracle) + oracle_data = await drift_acct.get_oracle_price_data(market.amm.oracle) current_price = oracle_data.price/PRICE_PRECISION current_pos_raw = (await chu.get_user_position(market_index)) if current_pos_raw is not None: @@ -127,7 +126,7 @@ async def main( else: market = await get_spot_market_account( drift_acct.program, market_index) - oracle_data = await get_oracle_data(connection, market.oracle) + oracle_data = await drift_acct.get_oracle_price_data(market.oracle) current_price = oracle_data.price/PRICE_PRECISION spot_pos = await chu.get_user_spot_position(market_index) diff --git a/requirements.txt b/requirements.txt index bec5ba2..0edd006 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ aiodns==3.0.0 aiohttp==3.8.3 aiosignal==1.3.1 -anchorpy==0.10.0 +anchorpy==0.17.1 +anchorpy-core==0.1.2 anyio==3.6.2 apischema==0.17.5 async-timeout==4.0.2 @@ -19,7 +20,8 @@ click==8.1.3 construct==2.10.68 construct-typing==0.5.3 dnspython==2.2.1 -driftpy==0.6.36 +driftpy==0.7.5 +exceptiongroup==1.0.4 flake8==6.0.0 frozenlist==1.3.3 ghp-import==2.1.0 @@ -28,7 +30,8 @@ httpcore==0.16.3 httpx==0.23.1 idna==3.4 iniconfig==1.1.1 -Jinja2==3.1.2 +Jinja2==3.0.3 +jsonalias==0.1.1 jsonrpcclient==4.0.2 jsonrpcserver==5.0.9 jsonschema==4.17.3 @@ -40,6 +43,8 @@ mergedeep==1.3.4 mkdocs==1.4.2 more-itertools==8.14.0 multidict==6.0.3 +mypy==1.7.0 +mypy-extensions==1.0.0 OSlash==0.6.3 packaging==22.0 pluggy==1.0.0 @@ -51,8 +56,8 @@ pycparser==2.21 pyflakes==3.0.1 pyheck==0.1.5 pyrsistent==0.19.2 -pytest==6.2.5 -pytest-asyncio==0.17.2 +pytest==7.4.3 +pytest-asyncio==0.21.1 pytest-xprocess==0.18.1 pythclient==0.1.4 python-dateutil==2.8.2 @@ -62,10 +67,11 @@ requests==2.28.1 rfc3986==1.5.0 six==1.16.0 sniffio==1.3.0 -solana==0.25.1 -solders==0.2.0 +solana==0.30.1 +solders==0.17.0 sumtypes==0.1a6 toml==0.10.2 +tomli==2.0.1 toolz==0.11.2 types-cachetools==4.2.10 types-requests==2.28.11.5 @@ -75,4 +81,4 @@ urllib3==1.26.13 watchdog==2.2.0 websockets==10.4 yarl==1.8.2 -zstandard==0.17.0 +zstandard==0.18.0 diff --git a/websocket_jit_maker.py b/websocket_jit_maker.py new file mode 100644 index 0000000..e1d99da --- /dev/null +++ b/websocket_jit_maker.py @@ -0,0 +1,230 @@ +# import ccxt.async_support as ccxt +import asyncio +import websockets +import json +import httpx + +import os +import json +import copy + +import sys + +from anchorpy import Wallet +from anchorpy import Provider +from solders.keypair import Keypair +from solana.rpc.async_api import AsyncClient + +from driftpy.constants.config import configs +from driftpy.types import * +#MarketType, OrderType, OrderParams, PositionDirection, OrderTriggerCondition + +from driftpy.drift_client import DriftClient +from driftpy.constants.numeric_constants import BASE_PRECISION, PRICE_PRECISION +from borsh_construct.enum import _rust_enum +import os + +# @dataclass +# class MakerInfo: +# maker: Pubkey +# order: Order + +MAX_TS_BUFFER = 15 # five seconds + +async def check_position(drift_acct: DriftClient): + user = await drift_acct.get_user() + bb = user.perp_positions[0].base_asset_amount / 1e9 + return bb + + +def calculate_mark_premium(perp_market: PerpMarketAccount): + prem1 = perp_market.amm.last_mark_price_twap5min/1e6 - perp_market.amm.historical_oracle_data.last_oracle_price_twap5min/1e6 + prem2 = perp_market.amm.last_mark_price_twap/1e6 - perp_market.amm.historical_oracle_data.last_oracle_price_twap/1e6 + return min(prem1, prem2) + + +def calculate_reservation_offset(inventory, max_inventory, ref_price, mark_premium): + return (-inventory/max(abs(inventory), max_inventory)) * .0001 * ref_price + mark_premium + # try: + # # rr = httpx.get('https://mainnet-beta.api.drift.trade/dlob/l3?marketIndex=0&marketType=perp&depth=1').json() + # rr = httpx.get('https://mainnet-beta.api.drift.trade/dlob/l2?marketIndex=0&marketType=perp&depth=1&includeVamm=True').json() + # bb = int(rr['bids'][0]['price'])/1e6 + # bo = int(rr['asks'][0]['price'])/1e6 + # bid = min(bb + .001, bid) + # ask = max(bo - .001, ask) + # except Exception as e: + # print(e) + # print('ERROR: with l3 drift data') + +async def do_trade(drift_acct: DriftClient, market_index, bid, ask): + current_position = await check_position(drift_acct) + # last_slot = drift_acct.account_subscriber.cache['state'].slot + # last_slot = None + # if last_slot is None: + # last_slot = (await drift_acct.program.provider.connection.get_slot()).value + # print(last_slot) + perp_market = await drift_acct.get_perp_market(market_index) + rev_offset = calculate_reservation_offset(current_position, + 1, + (bid+ask)/2, + calculate_mark_premium(perp_market) + ) + bid += rev_offset + ask += rev_offset + print(f'Quoting Price: {bid:.3f}/{ask:.3f}') + + + max_ts = None # (await drift_acct.program.provider.connection.get_block_time(last_slot)).value + MAX_TS_BUFFER + default_order_params = OrderParams( + order_type=OrderType.LIMIT(), + market_type=MarketType.PERP(), + direction=PositionDirection.LONG(), + user_order_id=0, + base_asset_amount=int(.1 * BASE_PRECISION), + price=0, + market_index=market_index, + reduce_only=False, + post_only=PostOnlyParams.NONE(), + immediate_or_cancel=False, + trigger_price=0, + trigger_condition=OrderTriggerCondition.ABOVE(), + oracle_price_offset=0, + auction_duration=None, + max_ts=max_ts, + auction_start_price=None, + auction_end_price=None, + ) + bid_order_params = copy.deepcopy(default_order_params) + bid_order_params.direction = PositionDirection.LONG() + bid_order_params.price = int(bid * PRICE_PRECISION - 1) + + ask_order_params = copy.deepcopy(default_order_params) + ask_order_params.direction = PositionDirection.SHORT() + ask_order_params.price = int(ask * PRICE_PRECISION + 1) + # maker_info = MakerInfo(maker=Pubkey('TODO'), order=None) + orders = [] + # print('POSTIION =', current_position) + if current_position > -1: + orders.append(ask_order_params) + if current_position < 1: + orders.append(bid_order_params) + # print(orders) + instr = await drift_acct.get_place_perp_orders_ix(orders) + + await drift_acct.send_ixs(instr) + +def calculate_weighted_average_price(bids, asks): + total_bid_price = 0 + total_bid_quantity = 0 + total_ask_price = 0 + total_ask_quantity = 0 + + for bid in bids: + price, quantity = map(float, bid) + total_bid_price += price * quantity + total_bid_quantity += quantity + + for ask in asks: + price, quantity = map(float, ask) + total_ask_price += price * quantity + total_ask_quantity += quantity + + if total_bid_quantity == 0 or total_ask_quantity == 0: + return None, None # Avoid division by zero + + weighted_average_bid_price = total_bid_price / total_bid_quantity + weighted_average_ask_price = total_ask_price / total_ask_quantity + + return weighted_average_bid_price, weighted_average_ask_price + +async def handle_binance_depth_feed(keypath, url, symbol, levels, update_speed): + # Initialize the Binance exchange object + # exchange = ccxt.binance() + market_index = 0 + subaccount_id = 0 + env = 'mainnet' + with open(os.path.expanduser(keypath), 'r') as f: secret = json.load(f) + kp: Pubkey = Keypair.from_bytes(bytes(secret)) + print('using address:', kp.pubkey(), 'subaccount =', subaccount_id) + config = configs[env] + wallet = Wallet(kp) + connection = AsyncClient(url) + provider = Provider(connection, wallet) + drift_acct = DriftClient.from_config(config, provider) + await drift_acct.account_subscriber.cache_if_needed() + + # Subscribe to the bid/ask depth updates + depth_channel = f'{symbol.lower()}@depth{levels}@{update_speed}' + url = f'wss://stream.binance.com:9443/ws/{depth_channel}' + print('Starting...') + last_bid, last_ask = None, None + + + while True: + try: + async with websockets.connect(url) as websocket: + while True: + message = await websocket.recv() + data = json.loads(message) + bids = data['bids'][:3] # Top bids + asks = data['asks'][:3] # Top asks + + # Calculate weighted average prices + weighted_average_bid_price, weighted_average_ask_price = calculate_weighted_average_price(bids, asks) + if last_bid is None: + last_bid = weighted_average_bid_price + if last_ask is None: + last_ask = weighted_average_ask_price + if weighted_average_bid_price is not None and weighted_average_ask_price is not None: + + if abs(last_ask - weighted_average_ask_price)/last_ask > .0001 or abs(last_bid - weighted_average_bid_price)/last_bid > .0001: + print(f'Wgt Price: {weighted_average_bid_price:.3f}/{weighted_average_ask_price:.3f}') + await do_trade(drift_acct, market_index, weighted_average_bid_price, weighted_average_ask_price) + last_bid = weighted_average_bid_price + last_ask = weighted_average_ask_price + else: + print('...') + + except Exception as e: + print(f'An error occurred: {str(e)}') + await asyncio.sleep(5) # Wait for 5 seconds before retrying + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('--keypath', type=str, required=False, default=os.environ.get('ANCHOR_WALLET')) + parser.add_argument('--env', type=str, default='devnet') + parser.add_argument('--url', type=str,) + parser.add_argument('--market', type=str, required=True, default='SOLUSDT') # todo + parser.add_argument('--min-position', type=float, required=False, default=None) # todo + parser.add_argument('--max-position', type=float, required=False, default=None) # todo + parser.add_argument('--subaccount', type=int, required=False, default=0) + parser.add_argument('--authority', type=str, required=False, default=None) # todo + args = parser.parse_args() + + # assert(args.spread > 0, 'spread must be > $0') + # assert(args.spread+args.offset < 2000, 'Invalid offset + spread (> $2000)') + + if args.keypath is None: + if os.environ['ANCHOR_WALLET'] is None: + raise NotImplementedError("need to provide keypath or set ANCHOR_WALLET") + else: + args.keypath = os.environ['ANCHOR_WALLET'] + + if args.env == 'devnet': + url = 'https://api.devnet.solana.com' + elif args.env == 'mainnet': + url = 'https://api.mainnet-beta.solana.com' + else: + raise NotImplementedError('only devnet/mainnet env supported') + + if args.url: + url = args.url + + print(args.env, url) + + keypath = args.keypath + symbol = args.market # Replace with the desired trading pair symbol + levels = 5 # You can change this to 5, 10, or 20 + update_speed = '100ms' # You can change this to '1000ms' or '100ms' + asyncio.get_event_loop().run_until_complete(handle_binance_depth_feed(keypath, url, symbol, levels, update_speed)) \ No newline at end of file