From df938fca4d3998449ad7df5a2d54963f500aa175 Mon Sep 17 00:00:00 2001 From: Ch1cK3N_t4c0s <51533784+jaredrsommer@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:03:40 -0400 Subject: [PATCH 1/5] Add files via upload --- blank_config.json | 18 ++++++++++++++++++ soltrade.py | 8 ++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 blank_config.json diff --git a/blank_config.json b/blank_config.json new file mode 100644 index 0000000..27fdd71 --- /dev/null +++ b/blank_config.json @@ -0,0 +1,18 @@ +{ + "api_key": "", + "private_key": "", + "custom_rpc_https": "", + "other_mint": "So11111111111111111111111111111111111111112", + "other_mint_symbol": "SOL", + "price_update_seconds": "60", + "trading_interval_minutes": "15", + "verbose": true, + "strategy": "default", + "sell_at_loss": false, + "stoploss": "20.0", + "trailing_stoploss_target": "5.0", + "trailing_stoploss": "1.5", + "telegram": true, + "tg_bot_token": "", + "tg_bot_uid": "" +} \ No newline at end of file diff --git a/soltrade.py b/soltrade.py index 5b87ab2..6eeb4be 100644 --- a/soltrade.py +++ b/soltrade.py @@ -1,19 +1,20 @@ +import asyncio from soltrade.wallet import find_balance from soltrade.config import config from soltrade.trading import start_trading from soltrade.log import log_general - # Initialize configuration config() def check_json_state() -> bool: if config().keypair and config().secondary_mint: return True + return False # Prints "Soltrade" and information about the connected wallet -print(""" $$\ $$\ $$\ +splash = (""" $$\ $$\ $$\ $$ | $$ | $$ | $$$$$$$\ $$$$$$\ $$ |$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$$ | $$$$$$\ $$ _____|$$ __$$\ $$ |\_$$ _| $$ __$$\ \____$$\ $$ __$$ |$$ __$$\ @@ -22,6 +23,8 @@ def check_json_state() -> bool: $$$$$$$ |\$$$$$$ |$$ | \$$$$ |$$ | \$$$$$$$ |\$$$$$$$ |\$$$$$$$\ \_______/ \______/ \__| \____/ \__| \_______| \_______| \_______| """) + +print(splash) can_run = check_json_state() # Error catching in case the program is unable to find the properties of the wallet @@ -34,6 +37,7 @@ def check_json_state() -> bool: # Checks if the run prompt should be displayed if can_run: log_general.debug("Soltrade has successfully imported the API requirements.") + start_trading() else: exit() \ No newline at end of file From d866653bd30878736c9b93b778b0ae7c78b5f3f5 Mon Sep 17 00:00:00 2001 From: Ch1cK3N_t4c0s <51533784+jaredrsommer@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:05:12 -0400 Subject: [PATCH 2/5] Add files via upload --- soltrade/config.py | 21 ++++++- soltrade/strategy.py | 89 +++++++++++++++++++++++++++++ soltrade/tg_bot.py | 13 +++++ soltrade/trading.py | 120 ++++++++++++++++++++------------------- soltrade/transactions.py | 27 +++++++++ 5 files changed, 212 insertions(+), 58 deletions(-) create mode 100644 soltrade/strategy.py create mode 100644 soltrade/tg_bot.py diff --git a/soltrade/config.py b/soltrade/config.py index 6c10822..2153290 100644 --- a/soltrade/config.py +++ b/soltrade/config.py @@ -25,6 +25,12 @@ def __init__(self): self.trading_interval_minutes = None self.slippage = None # BPS self.computeUnitPriceMicroLamports = None + self.stoploss = None + self.trailing_stoploss = None + self.trailing_stoploss_target = None + self.telegram = None + self.tg_bot_token = None + self.tg_bot_uid = None self.load_config() def load_config(self): @@ -41,11 +47,24 @@ def load_config(self): # DEFAULT FEE OF ROUGHLY $0.04 TODAY self.computeUnitPriceMicroLamports = int(os.getenv("COMPUTE_UNIT_PRICE_MICRO_LAMPORTS") or 20 * 14000) + + #with open(self.path, 'r') as file: + # try: + # config_data = json.load(file) + # self.verbose = config_data.get("verbose", False) + # self.strategy = config_data.get("strategy", "default") + # self.stoploss = config_data["stoploss"] + # self.trailing_stoploss = config_data["trailing_stoploss"] + # self.trailing_stoploss_target = config_data["trailing_stoploss_target"] @property def keypair(self) -> Keypair: try: - return Keypair.from_bytes(base58.b58decode(self.private_key)) + b58_string = self.private_key + keypair = Keypair.from_base58_string(b58_string) + # print(f"Using Wallet: {keypair.pubkey()}") + + return keypair except Exception as e: log_general.error(f"Error decoding private key: {e}") exit(1) diff --git a/soltrade/strategy.py b/soltrade/strategy.py new file mode 100644 index 0000000..f9fd26d --- /dev/null +++ b/soltrade/strategy.py @@ -0,0 +1,89 @@ +import pandas as pd +import pandas_ta as pta +import talib.abstract as ta +from soltrade.config import config +from soltrade.log import log_general + +def strategy(df: pd.DataFrame): + if config().strategy == 'default' or None: + if config().strategy is None: + log_general.info("No strategy selected in config.json using default") + + ### Populate default indicators + # Calculates EMA + df['ema_s'] = ta.EMA(df['close'], timeperiod = 8) + df['ema_m'] = ta.EMA(df['close'], timeperiod = 21) + + # Bollinger Bands + sma = ta.SMA(df['close'], timeperiod = 20) + std = df['close'].rolling(20).std() + df['upper_bband'] = sma + std * 2 + df['lower_bband'] = sma - std * 2 + + # RSI + df['rsi'] = ta.RSI(df, timeperiod=14) + + + ### Entry + entry = ( + ((df['ema_s'] > df['ema_m']) | + (df['close'] < df['lower_bband'])) & + (df['rsi'] <= 31) + ) + df.loc[entry, 'entry'] = 1 + + + ### Exit + exit = ( + ((df['ema_s'] < df['ema_m']) | + (df['close'] > df['upper_bband'])) & + (df['rsi'] >= 65) + ) + df.loc[exit, 'exit'] = 1 + + return df + + if config().strategy == 'custom': + + df['cci'] = ta.CCI(df) + + + +def calc_stoploss(df): + # Can also write custom stoploss functions using indicators. + sl = float(config().stoploss) + + df['stoploss'] = df['entry_price'].iloc[-1] * (1 - sl / 100) + + return df + +def calc_trailing_stoploss(df): + # Can also write custom stoploss functions using indicators. + tsl = float(config().trailing_stoploss) + tslt = float(config().trailing_stoploss_target) + + high_prices = df['high'] + df['entry_price'] * (1 + tslt / 100) + trailing_stop = [] + tracking_started = False # Flag to indicate when to start tracking the trailing stop-loss + highest_price = df['high'].iloc[0] + + for price in high_prices: + if not tracking_started and price >= df['entry_price'].iloc[0] * (1 + tslt / 100): + # Start tracking the trailing stop-loss once the target price is reached + tracking_started = True + stop_price = highest_price * (1 - tsl / 100) + elif tracking_started: + if price > highest_price: + highest_price = price + stop_price = highest_price * (1 - tsl / 100) + + trailing_stop.append(stop_price if tracking_started else None) + + df['trailing_stoploss'] = trailing_stop + df['trailing_stoploss_target'] = df['entry_price'] * (1 + tslt / 100) + + return df + + + diff --git a/soltrade/tg_bot.py b/soltrade/tg_bot.py new file mode 100644 index 0000000..ba09b7f --- /dev/null +++ b/soltrade/tg_bot.py @@ -0,0 +1,13 @@ +import asyncio +import telegram +from soltrade.config import config +from telegram.ext import ApplicationBuilder + +# bot_token = config().tg_bot_token +# chat = config().tg_bot_uid + +async def send_info(msg): + bot_token = config().tg_bot_token + chat = config().tg_bot_uid + application = ApplicationBuilder().token(bot_token).build() + await application.bot.sendMessage(chat_id=chat, text=msg, parse_mode="HTML") diff --git a/soltrade/trading.py b/soltrade/trading.py index 47e4bb7..3bcf65d 100644 --- a/soltrade/trading.py +++ b/soltrade/trading.py @@ -6,9 +6,11 @@ from soltrade.transactions import perform_swap, market from soltrade.indicators import calculate_ema, calculate_rsi, calculate_bbands +from soltrade.strategy import strategy, calc_stoploss, calc_trailing_stoploss from soltrade.wallet import find_balance from soltrade.log import log_general, log_transaction from soltrade.config import config +from soltrade.tg_bot import send_info market('position.json') @@ -27,7 +29,7 @@ def fetch_candlestick() -> dict: def perform_analysis(): log_general.debug("Soltrade is analyzing the market; no trade has been executed.") - global stoploss, takeprofit + global stoploss, takeprofit, trailing_stoploss, entry_price market().load_position() @@ -36,69 +38,73 @@ def perform_analysis(): candle_dict = candle_json["Data"]["Data"] # Creates DataFrame for manipulation - columns = ['close', 'high', 'low', 'open', 'time', 'VF', 'VT'] + columns = ['close', 'high', 'low', 'open', 'time'] df = pd.DataFrame(candle_dict, columns=columns) df['time'] = pd.to_datetime(df['time'], unit='s') + + df = strategy(df) + print(df.tail(2)) - # DataFrame variable for TA-Lib manipulation - cl = df['close'] - - # Technical analysis values used in trading algorithm - price = cl.iat[-1] - ema_short = calculate_ema(dataframe=df, length=5) - ema_medium = calculate_ema(dataframe=df, length=20) - rsi = calculate_rsi(dataframe=df, length=14) - upper_bb, lower_bb = calculate_bbands(dataframe=df, length=14) - stoploss = market().sl - takeprofit = market().tp - - log_general.debug(f""" -price: {price} -short_ema: {ema_short} -med_ema: {ema_medium} -upper_bb: {upper_bb.iat[-1]} -lower_bb: {lower_bb.iat[-1]} -rsi: {rsi} -stop_loss {stoploss} -take_profit: {takeprofit} -""") - - if not market().position: + if not MarketPosition().position: input_amount = find_balance(config().primary_mint) - - if (ema_short > ema_medium or price < lower_bb.iat[-1]) and rsi <= 31: - log_transaction.info("Soltrade has detected a buy signal.") + if df['entry'].iloc[-1] == 1: + buy_msg = f"Soltrade has detected a buy signal using {input_amount} ${config().primary_mint_symbol}." + log_transaction.info(buy_msg) + # log_transaction.info(get_statistics()) if input_amount <= 0: - log_transaction.warning(f"Soltrade has detected a buy signal, but does not have enough {config().primary_mint_symbol} to trade.") + fund_msg = f"Soltrade has detected a buy signal, but does not have enough ${config().primary_mint_symbol} to trade." + log_transaction.info(fund_msg) return - is_swapped = asyncio.run(perform_swap(input_amount, config().primary_mint)) - if is_swapped: - stoploss = market().sl = cl.iat[-1] * 0.925 - takeprofit = market().tp = cl.iat[-1] * 1.25 - market().update_position(True, stoploss, takeprofit) - return - else: + asyncio.run(perform_swap(input_amount, config().primary_mint)) + df['entry_price'] = df['close'].iloc[-1] + entry_price = df['entry_price'] + df = calc_stoploss(df) + df = calc_trailing_stoploss(df) + stoploss = df['stoploss'].iloc[-1] + trailing_stoploss = df['trailing_stoploss'].iloc[-1] + print(df.tail(2)) + # Save DataFrame to JSON file + json_file_path = 'data.json' + save_dataframe_to_json(df, json_file_path) + + + else: + # Read DataFrame from JSON file + df = read_dataframe_from_json(json_file_path) + print(df.tail(2)) input_amount = find_balance(config().secondary_mint) - - if price <= stoploss or price >= takeprofit: - log_transaction.info("Soltrade has detected a sell signal. Stoploss or takeprofit has been reached.") - is_swapped = asyncio.run(perform_swap(input_amount, config().secondary_mint)) - if is_swapped: - stoploss = takeprofit = market().sl = market().tp = 0 - market().update_position(False, stoploss, takeprofit) - return - - if (ema_short < ema_medium or price > upper_bb.iat[-1]) and rsi >= 68: - log_transaction.info("Soltrade has detected a sell signal. EMA or BB has been reached.") - is_swapped = asyncio.run(perform_swap(input_amount, config().secondary_mint)) - if is_swapped: - stoploss = takeprofit = market().sl = market().tp = 0 - market().update_position(False, stoploss, takeprofit) - return - -# This starts the trading function on a timer -def start_trading(): - log_general.info("Soltrade has now initialized the trading algorithm.") + df = calc_trailing_stoploss(df) + stoploss = df['stoploss'].iloc[-1] + trailing_stoploss = df['trailing_stoploss'].iloc[-1] + print(stoploss, trailing_stoploss) + + # Check Stoploss + if df['close'].iloc[-1] <= stoploss: + sl_msg = f"Soltrade has detected a sell signal for {input_amount} ${config().secondary_mint_symbol}. Stoploss has been reached." + log_transaction.info(sl_msg) + # log_transaction.info(get_statistics()) + asyncio.run(perform_swap(input_amount, config().secondary_mint)) + stoploss = takeprofit = 0 + df['entry_price'] = None + + # Check Trailing Stoploss + if trailing_stoploss is not None: + if df['close'].iloc[-1] < trailing_stoploss: + tsl_msg = f"Soltrade has detected a sell signal for {input_amount} ${config().secondary_mint_symbol}. Trailing stoploss has been reached." + log_transaction.info(tsl_msg) + # log_transaction.info(get_statistics()) + asyncio.run(perform_swap(input_amount, config().secondary_mint)) + stoploss = takeprofit = 0 + df['entry_price'] = None + + # Check Strategy + if df['exit'].iloc[-1] == 1: + exit_msg = f"Soltrade has detected a sell signal for {input_amount} ${config().secondary_mint_symbol}.." + log_transaction.info(exit_msg) + # log_transaction.info(get_statistics()) + asyncio.run(perform_swap(input_amount, config().secondary_mint)) + stoploss = takeprofit = 0 + df['entry_price'] = None trading_sched = BlockingScheduler() trading_sched.add_job(perform_analysis, 'interval', seconds=config().price_update_seconds, max_instances=1) diff --git a/soltrade/transactions.py b/soltrade/transactions.py index 43463b1..c2ec825 100644 --- a/soltrade/transactions.py +++ b/soltrade/transactions.py @@ -12,6 +12,7 @@ from soltrade.log import log_general, log_transaction from soltrade.config import config +from soltrade.tg_bot import send_info class MarketPosition: @@ -96,6 +97,29 @@ async def create_transaction(quote: dict) -> dict: response = await client.post("https://quote-api.jup.ag/v6/swap", json=parameters) return response.json() +def get_transaction_details(tx_id): + # RPC endpoint URL + rpc_url = 'https://api.mainnet-beta.solana.com' + + # Construct the JSON-RPC request + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "getTransaction", + "params": [tx_id], + } + + # Send the request + response = requests.post(rpc_url, json=payload) + + if response.status_code == 200: + result = response.json() + if 'result' in result: + return result['result'] + else: + print("Error: Transaction not found") + else: + print("Error: Failed to retrieve transaction details") # Deserializes and sends the transaction from the swap information given def send_transaction(swap_transaction: dict, opts: TxOpts) -> Signature: @@ -104,8 +128,11 @@ def send_transaction(swap_transaction: dict, opts: TxOpts) -> Signature: signed_txn = VersionedTransaction.populate(raw_txn.message, [signature]) result = config().client.send_raw_transaction(bytes(signed_txn), opts) + print(result) txid = result.value log_transaction.info(f"Soltrade TxID: {txid}") + verified_tx = get_transaction_details(tx_id) + log_transaction.info(f"Soltrade Verified on Chain! {verified_tx}") return txid def find_transaction_error(txid: Signature) -> dict: From 2508e4d128c34b42fa8b1534bc4886962554cdf2 Mon Sep 17 00:00:00 2001 From: noahtheprogrammer Date: Tue, 23 Jul 2024 20:25:48 -0400 Subject: [PATCH 3/5] Removed redundant asynchronous functions While these functions will probably be implemented in a later version of Soltrade, this branch primarily focuses on the strategy creation feature. In addition, they are not conditional and will always attempt to execute, even if Telegram is not being used. --- soltrade.py | 1 - soltrade/tg_bot.py | 13 ------------- soltrade/trading.py | 1 - soltrade/transactions.py | 2 -- 4 files changed, 17 deletions(-) delete mode 100644 soltrade/tg_bot.py diff --git a/soltrade.py b/soltrade.py index 6eeb4be..7e88d7f 100644 --- a/soltrade.py +++ b/soltrade.py @@ -1,4 +1,3 @@ -import asyncio from soltrade.wallet import find_balance from soltrade.config import config from soltrade.trading import start_trading diff --git a/soltrade/tg_bot.py b/soltrade/tg_bot.py deleted file mode 100644 index ba09b7f..0000000 --- a/soltrade/tg_bot.py +++ /dev/null @@ -1,13 +0,0 @@ -import asyncio -import telegram -from soltrade.config import config -from telegram.ext import ApplicationBuilder - -# bot_token = config().tg_bot_token -# chat = config().tg_bot_uid - -async def send_info(msg): - bot_token = config().tg_bot_token - chat = config().tg_bot_uid - application = ApplicationBuilder().token(bot_token).build() - await application.bot.sendMessage(chat_id=chat, text=msg, parse_mode="HTML") diff --git a/soltrade/trading.py b/soltrade/trading.py index 3bcf65d..1b83082 100644 --- a/soltrade/trading.py +++ b/soltrade/trading.py @@ -10,7 +10,6 @@ from soltrade.wallet import find_balance from soltrade.log import log_general, log_transaction from soltrade.config import config -from soltrade.tg_bot import send_info market('position.json') diff --git a/soltrade/transactions.py b/soltrade/transactions.py index c2ec825..81af1e6 100644 --- a/soltrade/transactions.py +++ b/soltrade/transactions.py @@ -12,8 +12,6 @@ from soltrade.log import log_general, log_transaction from soltrade.config import config -from soltrade.tg_bot import send_info - class MarketPosition: def __init__(self, path): From 88724d1c1c87b2a421a4bf0f8b85bd640da84f3f Mon Sep 17 00:00:00 2001 From: noahtheprogrammer Date: Tue, 23 Jul 2024 23:26:30 -0400 Subject: [PATCH 4/5] Resolved conflicts between repositories The conflicts required for merging have been resolved, but the branch is by no means ready for main. --- soltrade/trading.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/soltrade/trading.py b/soltrade/trading.py index 1b83082..54ccd3e 100644 --- a/soltrade/trading.py +++ b/soltrade/trading.py @@ -39,8 +39,7 @@ def perform_analysis(): # Creates DataFrame for manipulation columns = ['close', 'high', 'low', 'open', 'time'] df = pd.DataFrame(candle_dict, columns=columns) - df['time'] = pd.to_datetime(df['time'], unit='s') - + df['time'] = pd.to_datetime(df['time'], unit='s') df = strategy(df) print(df.tail(2)) @@ -98,14 +97,29 @@ def perform_analysis(): # Check Strategy if df['exit'].iloc[-1] == 1: - exit_msg = f"Soltrade has detected a sell signal for {input_amount} ${config().secondary_mint_symbol}.." + exit_msg = f"Soltrade has detected a sell signal for {input_amount} ${config().secondary_mint_symbol}." log_transaction.info(exit_msg) # log_transaction.info(get_statistics()) asyncio.run(perform_swap(input_amount, config().secondary_mint)) stoploss = takeprofit = 0 df['entry_price'] = None +# This starts the trading function on a timer +def start_trading(): + log_general.info("Soltrade has now initialized the trading algorithm.") trading_sched = BlockingScheduler() trading_sched.add_job(perform_analysis, 'interval', seconds=config().price_update_seconds, max_instances=1) trading_sched.start() - perform_analysis() \ No newline at end of file + perform_analysis() + +# Function to save DataFrame to JSON file +def save_dataframe_to_json(df, file_path): + df_json = df.to_json(orient='records') + with open(file_path, 'w') as f: + json.dump(df_json, f) + +# Function to read DataFrame from JSON file +def read_dataframe_from_json(file_path): + with open(file_path, 'r') as f: + df_json = json.load(f) + return pd.read_json(df_json) From b8dd7602b3993f1f6a8ebfe4d82077094c6cce4c Mon Sep 17 00:00:00 2001 From: noahtheprogrammer Date: Thu, 25 Jul 2024 22:26:55 -0400 Subject: [PATCH 5/5] Removed indicators.py and changed requirements.txt The indicators file is no longer necessary due to the addition of a pandas-only technical analysis library. --- requirements.txt | 3 ++- soltrade/indicators.py | 28 ---------------------------- soltrade/strategy.py | 7 +------ 3 files changed, 3 insertions(+), 35 deletions(-) delete mode 100644 soltrade/indicators.py diff --git a/requirements.txt b/requirements.txt index 3181a13..83e61e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ requests==2.31 solana==0.29.0 matplotlib==3.8 solders==0.14.4 -python-dotenv==1.0.1 \ No newline at end of file +python-dotenv==1.0.1 +pandas_ta==0.3.14b0 \ No newline at end of file diff --git a/soltrade/indicators.py b/soltrade/indicators.py deleted file mode 100644 index 9846f55..0000000 --- a/soltrade/indicators.py +++ /dev/null @@ -1,28 +0,0 @@ -import pandas as pd - - -# Calculates EMA using DataFrame -def calculate_ema(dataframe: pd.DataFrame, length: int) -> int: - ema = dataframe['close'].ewm(span=length, adjust=False).mean() - return ema.iat[-1] - - -# Calculates BB using SMA indicator and DataFrame -def calculate_bbands(dataframe: pd.DataFrame, length: int) -> pd.Series: - sma = dataframe['close'].rolling(length).mean() - std = dataframe['close'].rolling(length).std() - upper_bband = sma + std * 2 - lower_bband = sma - std * 2 - return upper_bband, lower_bband - - -# Calculates RSI using custom EMA indicator and DataFrame -def calculate_rsi(dataframe: pd.DataFrame, length: int) -> int: - delta = dataframe['close'].diff() - up = delta.clip(lower=0) - down = delta.clip(upper=0).abs() - upper_ema = up.ewm(com=length - 1, adjust=False, min_periods=length).mean() - lower_ema = down.ewm(com=length - 1, adjust=False, min_periods=length).mean() - rsi = upper_ema / lower_ema - rsi = 100 - (100 / (1 + rsi)) - return rsi.iat[-1] diff --git a/soltrade/strategy.py b/soltrade/strategy.py index f9fd26d..6d3f3df 100644 --- a/soltrade/strategy.py +++ b/soltrade/strategy.py @@ -46,8 +46,6 @@ def strategy(df: pd.DataFrame): if config().strategy == 'custom': df['cci'] = ta.CCI(df) - - def calc_stoploss(df): # Can also write custom stoploss functions using indicators. @@ -83,7 +81,4 @@ def calc_trailing_stoploss(df): df['trailing_stoploss'] = trailing_stop df['trailing_stoploss_target'] = df['entry_price'] * (1 + tslt / 100) - return df - - - + return df \ No newline at end of file