├── .env ├── Automated.py ├── Manual.py ├── README.md ├── Updated_Manual.py ├── installation.md ├── layouts.py └── requirements.txt /.env: -------------------------------------------------------------------------------- 1 | ## Path: MANUAL SETTINGS 2 | 3 | wallet_address="FJRZ5sTp27n6GhUVqgVkY4JGUJPjhRPnWtH4du5UhKbw" 4 | RPC_HTTPS_URL="" #Your RPC URL 5 | max_token_accounts= "1500" #eg Max number of token accounts a wallet can have 6 | 7 | 8 | automated_wallet_address="675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" #Raydium Pool V4 Program ID 9 | 10 | 11 | -------------------------------------------------------------------------------- /Automated.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pandas as pd 3 | from openpyxl.styles import PatternFill 4 | from openpyxl.utils import get_column_letter 5 | from openpyxl.styles import Font, Border, Side, Alignment 6 | from solders.rpc.responses import GetTransactionResp 7 | import layouts 8 | import asyncio 9 | import datetime 10 | import websockets 11 | import json 12 | from solders.signature import Signature 13 | from solana.rpc.async_api import AsyncClient 14 | from datetime import datetime 15 | from spl.token.constants import TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT 16 | from solana.rpc import types 17 | from solana.rpc.types import TokenAccountOpts 18 | from solders.pubkey import Pubkey 19 | import sqlite3 20 | import requests 21 | from dotenv import dotenv_values 22 | config = dotenv_values(".env") 23 | 24 | class style(): 25 | BLACK = '\033[30m' 26 | RED = '\033[31m' 27 | GREEN = '\033[32m' 28 | YELLOW = '\033[33m' 29 | BLUE = '\033[34m' 30 | MAGENTA = '\033[35m' 31 | CYAN = '\033[36m' 32 | WHITE = '\033[37m' 33 | UNDERLINE = '\033[4m' 34 | RESET = '\033[0m' 35 | 36 | 37 | # wallet_address = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" # 38 | seen_signatures = set() 39 | Pool_raydium = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" 40 | banangun_fees="4BBNEVRgrxVKv9f7pMNE788XM1tt379X9vNjpDH2KCL7" 41 | solTrading_fees="HEPL5rTb6n1Ax6jt9z2XMPFJcDe9bSWvWQpsK7AMcbZg" 42 | raydium_V4 = "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1" 43 | 44 | 45 | class TransactionProcessor: 46 | def __init__(self): 47 | self.queue = asyncio.Queue() 48 | self.wallet_address = None 49 | self.async_solana_client = AsyncClient(config["RPC_HTTPS_URL"]) 50 | self.db_name = "automate.db" 51 | self.conn = sqlite3.connect(self.db_name) 52 | self.c = self.conn.cursor() 53 | self.init_db() 54 | self.income = 0 55 | self.outcome = 0 56 | self.fee = 0 57 | self.spent_sol = 0 58 | self.earned_sol = 0 59 | self.buys = 0 60 | self.sells = 0 61 | self.delta_sol=0 62 | self.delta_token=0 63 | self.delta_percentage=0 64 | self.first_buy_time = None 65 | self.last_sell_time = None 66 | self.last_trade = None 67 | self.time_period = 0 68 | self.contract = None 69 | self.scam_tokens = 0 70 | self.sol_balance = None 71 | self.solana_price = self.get_current_solana_price() 72 | self.tokenCreationTime = 0 73 | self.mint_decimal = None 74 | self.mint_address = None 75 | self.buy_period=0 76 | 77 | 78 | 79 | def init_db(self): 80 | self.c.execute(''' 81 | CREATE TABLE IF NOT EXISTS wallet_address ( 82 | id INTEGER PRIMARY KEY AUTOINCREMENT, 83 | wallet_address TEXT UNIQUE 84 | ) 85 | ''') 86 | self.c.execute(''' 87 | CREATE TABLE IF NOT EXISTS token_accounts ( 88 | wallet_address_id INTEGER, 89 | wallet_token_account TEXT, 90 | block_time INTEGER, 91 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 92 | ) 93 | ''') 94 | 95 | self.c.execute(''' 96 | CREATE TABLE IF NOT EXISTS pnl_info ( 97 | token_account TEXT PRIMARY KEY, 98 | wallet_address_id INTEGER, 99 | income REAL, 100 | outcome REAL, 101 | total_fee REAL, 102 | spent_sol REAL, 103 | earned_sol REAL, 104 | delta_token REAL, 105 | delta_sol REAL, 106 | delta_percentage REAL, 107 | buys INTEGER, 108 | sells INTEGER, 109 | last_trade TEXT, 110 | time_period TEXT, 111 | contract TEXT, 112 | scam_tokens TEXT, 113 | buy_period TEXT, 114 | 115 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 116 | ) 117 | ''') 118 | 119 | self.c.execute(''' 120 | CREATE TABLE IF NOT EXISTS winning_wallets ( 121 | wallet_address_id INTEGER, 122 | win_rate_7 REAL, 123 | balance_change_7 REAL, 124 | token_accounts_7 INTEGER, 125 | win_rate_14 REAL, 126 | balance_change_14 REAL, 127 | token_accounts_14 INTEGER, 128 | win_rate_30 REAL, 129 | balance_change_30 REAL, 130 | token_accounts_30 INTEGER, 131 | win_rate_60 REAL, 132 | balance_change_60 REAL, 133 | token_accounts_60 INTEGER, 134 | win_rate_90 REAL, 135 | balance_change_90 REAL, 136 | token_accounts_90 INTEGER, 137 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 138 | ) 139 | ''') 140 | self.conn.commit() 141 | 142 | async def get_token_accountsCount(self,wallet_address: Pubkey): 143 | owner = wallet_address 144 | opts = types.TokenAccountOpts(program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) 145 | response = await self.async_solana_client.get_token_accounts_by_owner(owner, opts) 146 | return len(response.value) 147 | 148 | 149 | async def initialize(self): 150 | self.sol_balance = await self.getSOlBalance() 151 | 152 | 153 | 154 | """REPORT GENERATION FUNCTIONS""" 155 | async def getSOlBalance(self): 156 | pubkey = self.wallet_address 157 | response = await self.async_solana_client.get_balance(pubkey) 158 | balance = response.value / 10**9 159 | return balance 160 | 161 | def get_current_solana_price(self): 162 | url = "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd" 163 | response = requests.get(url) 164 | data = response.json() 165 | return data['solana']['usd'] if response.status_code == 200 else None 166 | 167 | 168 | 169 | def store_win_rate(self, time_period, win_rate, balance_change, token_accounts): 170 | check_sql = 'SELECT 1 FROM winning_wallets WHERE wallet_address_id = ?' 171 | self.c.execute(check_sql, (self.wallet_address_id,)) 172 | row_exists = self.c.fetchone() is not None 173 | time_period_suffix = f"_{time_period}" 174 | 175 | if row_exists: 176 | update_sql = f''' 177 | UPDATE winning_wallets 178 | SET win_rate{time_period_suffix} = ?, 179 | balance_change{time_period_suffix} = ?, 180 | token_accounts{time_period_suffix} = ? 181 | WHERE wallet_address_id = ? 182 | ''' 183 | self.c.execute(update_sql, (win_rate, balance_change, token_accounts, self.wallet_address_id)) 184 | else: 185 | 186 | insert_sql = f''' 187 | INSERT INTO winning_wallets ( 188 | wallet_address_id, 189 | win_rate_7, balance_change_7, token_accounts_7, 190 | win_rate_14, balance_change_14, token_accounts_14, 191 | win_rate_30, balance_change_30, token_accounts_30, 192 | win_rate_60, balance_change_60, token_accounts_60, 193 | win_rate_90, balance_change_90, token_accounts_90 194 | ) VALUES ( 195 | ?, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL 196 | ) 197 | ''' 198 | self.c.execute(insert_sql, (self.wallet_address_id,)) 199 | update_sql = f''' 200 | UPDATE winning_wallets 201 | SET win_rate{time_period_suffix} = ?, 202 | balance_change{time_period_suffix} = ?, 203 | token_accounts{time_period_suffix} = ? 204 | WHERE wallet_address_id = ? 205 | ''' 206 | self.c.execute(update_sql, (win_rate, balance_change, token_accounts, self.wallet_address_id)) 207 | 208 | self.conn.commit() 209 | 210 | def get_summary(self, time_period): 211 | query = f''' 212 | WITH Calculations AS ( 213 | SELECT 214 | (SUM(CASE WHEN delta_sol > 0 THEN 1 ELSE 0 END) * 1.0 / COUNT(*)) * 100 AS WinRate, 215 | SUM(delta_sol) AS PnL_R, 216 | SUM(CASE WHEN delta_sol < 0 THEN delta_sol ELSE 0 END) AS PnL_Loss, 217 | (SUM(earned_sol) / NULLIF(SUM(spent_sol), 0) - 1) * 100 AS Balance_Change, 218 | COUNT(CASE WHEN scam_tokens = 1 THEN 1 END) AS ScamTokens, 219 | COUNT(token_account) AS TokenAccounts 220 | FROM pnl_info 221 | WHERE wallet_address_id = ? 222 | AND last_trade >= strftime('%s', 'now', '-{time_period} days') 223 | ) 224 | SELECT 225 | *, 226 | '{time_period} days' AS TimePeriod 227 | FROM Calculations; 228 | ''' 229 | 230 | self.c.execute(query, (self.get_wallet_address_id(self.wallet_address),)) 231 | 232 | summary_result = self.c.fetchone() 233 | win_rate = summary_result[0] 234 | balance_change = summary_result[3] 235 | count_token_accounts=summary_result[5] 236 | self.store_win_rate(time_period, win_rate,balance_change,count_token_accounts) 237 | 238 | 239 | 240 | summary_data = { 241 | 'SolBalance': self.sol_balance, 242 | 'WalletAddress': str(self.wallet_address), 243 | 'WinRate': summary_result[0], 244 | 'PnL_R': summary_result[1], 245 | 'PnL_Loss': summary_result[2], 246 | 'Balance_Change': summary_result[3], 247 | 'ScamTokens': summary_result[4], 248 | 'TimePeriod': summary_result[6] # 249 | } 250 | 251 | return summary_data 252 | def get_transactions(self, time_period): 253 | query = f''' 254 | SELECT * 255 | FROM pnl_info 256 | WHERE wallet_address_id = ? 257 | AND last_trade >= strftime('%s', 'now', '-{time_period} days'); 258 | ''' 259 | self.c.execute(query, (self.wallet_address_id,)) 260 | results = self.c.fetchall() 261 | transactions_df = pd.DataFrame(results, columns=['token_account', 'wallet_address_id', 'income', 'outcome', 262 | 'total_fee', 'spent_sol', 'earned_sol', 'delta_token', 263 | 'delta_sol', 'delta_percentage', 'buys', 'sells', 264 | 'last_trade', 'time_period', 'contract', 'scam token','buy_period']) 265 | 266 | # Sort transactions by 'last_trade' in descending order 267 | transactions_df = transactions_df.sort_values(by='last_trade', ascending=False) 268 | 269 | return transactions_df 270 | 271 | async def generate_reports_for_time_periods(self, wallet,time_periods): 272 | await self.initialize() 273 | self.wallet_address_id = self.get_wallet_address_id(wallet) 274 | reports_folder = "automate_reports" 275 | summary_30_days = self.get_summary(30) 276 | win_rate_30_days = summary_30_days['WinRate'] if summary_30_days else 'Unknown' 277 | if not os.path.exists(reports_folder): 278 | os.makedirs(reports_folder) 279 | wallet_folder = os.path.join(reports_folder, f"{win_rate_30_days}_days_{wallet}") 280 | if not os.path.exists(wallet_folder): 281 | os.makedirs(wallet_folder) 282 | 283 | for time_period in time_periods: 284 | summary = self.get_summary(time_period) 285 | transactions = self.get_transactions(time_period) 286 | if summary: 287 | file_name = os.path.join(wallet_folder, f"{self.wallet_address}_{time_period}_days.xlsx") 288 | self.export_to_excel(summary, transactions, file_name) 289 | print(f"Exported summary and transactions for {time_period} days to {file_name}") 290 | else: 291 | print(f"No summary found for {time_period} days.") 292 | 293 | def export_to_excel(self,summary, transactions, file_name): 294 | sol_current_price = self.solana_price 295 | summary['Current_Sol_Price'] = sol_current_price 296 | summary['ID'] = self.wallet_address_id 297 | try: 298 | summary['Profit_USD'] = abs(summary['PnL_R']) * sol_current_price 299 | summary['Loss_USD'] = abs(summary['PnL_Loss']) * sol_current_price 300 | except TypeError: 301 | summary['Profit_USD'] = 0 302 | summary['Loss_USD'] = 0 303 | 304 | summary_df = pd.DataFrame([summary], columns=['ID','WalletAddress', 'SolBalance','Current_Sol_Price', 'WinRate', 'PnL_R', 'PnL_Loss', 305 | 'Balance_Change', 'ScamTokens', 'Profit_USD', 306 | 'Loss_USD', 'TimePeriod']) 307 | 308 | transactions_df = pd.DataFrame(transactions, columns=['token_account','wallet_address_id', 'income', 'outcome', 309 | 'total_fee', 'spent_sol', 'earned_sol', 'delta_token', 310 | 'delta_sol', 'delta_percentage', 'buys', 'sells', 311 | 'last_trade', 'time_period','buy_period', 'contract', 'scam token']) 312 | transactions_df.drop(columns=['wallet_address_id'], inplace=True) 313 | transactions_df['last_trade'] = transactions_df['last_trade'].apply(lambda x: datetime.fromtimestamp(int(x)).strftime('%d.%m.%Y')) 314 | 315 | 316 | with pd.ExcelWriter(file_name, engine='openpyxl') as writer: 317 | summary_df.to_excel(writer, sheet_name='Summary and Transactions', index=False, startrow=0) 318 | row_to_start = len(summary_df) + 2 319 | transactions_df.to_excel(writer, sheet_name='Summary and Transactions', index=False, startrow=row_to_start) 320 | workbook = writer.book 321 | worksheet = writer.sheets['Summary and Transactions'] 322 | 323 | for row in worksheet.iter_rows(min_row=row_to_start + 2, max_col=worksheet.max_column): 324 | for cell in row: 325 | if cell.column == 1: # 'token_account' column 326 | cell.hyperlink = f'https://solscan.io/account/{cell.value}#splTransfer' 327 | cell.value = 'View Solscan' 328 | cell.font = Font(underline='single') 329 | elif cell.column == 15: # 'contract' column 330 | cell.hyperlink = f'https://dexscreener.com/solana/{cell.value}?maker={self.wallet_address}' 331 | 'https://dexscreener.com/solana/3zcoadmvqtx3itfthwr946nhznqh92eq9hdhhjtgp6as?maker=3uij3uDg5pLBBxQw6hUXxqw6uEwCQGX7rM8PZb7ofH9e' 332 | cell.value = 'View Dexscreener' 333 | cell.font = Font(underline='single') 334 | 335 | 336 | # Define fills for conditional formatting 337 | red_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") 338 | green_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid") 339 | yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") 340 | brown_fill = PatternFill(start_color="A52A2A", end_color="A52A2A", fill_type="solid") 341 | gold_fill = PatternFill(start_color="FFD700", end_color="FFD700", fill_type="solid") 342 | 343 | # Apply initial styling 344 | for row in worksheet.iter_rows(min_row=1, max_row=worksheet.max_row, min_col=1, 345 | max_col=worksheet.max_column): 346 | for cell in row: 347 | cell.border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), 348 | bottom=Side(style='thin')) 349 | cell.alignment = Alignment(horizontal="center", vertical="center") 350 | if cell.row == 1: 351 | cell.font = Font(bold=True) 352 | 353 | for column_cells in worksheet.columns: 354 | length = max(len(str(cell.value)) for cell in column_cells) 355 | worksheet.column_dimensions[get_column_letter(column_cells[0].column)].width = length + 2 356 | 357 | for idx, row in enumerate(transactions_df.itertuples(index=False), start=row_to_start + 2): 358 | outcome = row[2] 359 | income=row[1] 360 | delta_percentage = row[8] 361 | time_period = row[12] 362 | buys = row[9] 363 | 364 | 365 | if round(outcome,1) > round(income,1): 366 | worksheet.cell(row=idx, column=3).fill = yellow_fill 367 | 368 | 369 | if delta_percentage ==-100: 370 | 371 | worksheet.cell(row=idx, column=9).fill = brown_fill 372 | 373 | if pd.to_timedelta(time_period) < pd.Timedelta(minutes=1): 374 | worksheet.cell(row=idx, column=13).fill = yellow_fill 375 | 376 | if buys > 3: 377 | worksheet.cell(row=idx, column=10).fill = yellow_fill 378 | 379 | 380 | for idx, row in enumerate(transactions_df.itertuples(index=False), start=row_to_start + 2): 381 | # For delta_sol 382 | if row[7] < 0: 383 | worksheet.cell(row=idx, column=8).fill = red_fill 384 | elif row[7] > 0: 385 | worksheet.cell(row=idx, column=8).fill = green_fill 386 | 387 | if row[7] < 0 and row[8] != -100: 388 | worksheet.cell(row=idx, column=9).fill = red_fill 389 | elif row[8] > 0: 390 | worksheet.cell(row=idx, column=9).fill = green_fill 391 | 392 | 393 | for idx, row in enumerate(summary_df.itertuples(index=False), start=1): 394 | if row[9] > row[10] and row[4]>=50: # If Profit_USD > Loss_USD 395 | worksheet.cell(row=idx, column=10).fill = gold_fill 396 | worksheet.cell(row=idx, column=5).fill = gold_fill 397 | elif row[9] < row[10] and row[4]<50: # If Loss_USD> Profit_USD 398 | worksheet.cell(row=idx, column=10).fill = red_fill 399 | worksheet.cell(row=idx, column=5).fill = red_fill 400 | elif row[9] < row[10] and row[4] == 50: 401 | worksheet.cell(row=idx, column=10).fill = red_fill 402 | worksheet.cell(row=idx, column=5).fill = gold_fill 403 | else: 404 | worksheet.cell(row=idx, column=11).fill = red_fill 405 | worksheet.cell(row=idx, column=5).fill = red_fill 406 | workbook.save(file_name) 407 | 408 | 409 | 410 | 411 | def get_wallet_address_id(self,wallet_address): 412 | self.c.execute('SELECT id FROM wallet_address WHERE wallet_address = ?', (str(wallet_address),)) 413 | result = self.c.fetchone() 414 | if result: 415 | return result[0] 416 | else: 417 | self.c.execute('INSERT INTO wallet_address (wallet_address) VALUES (?)', (str(wallet_address),)) 418 | self.conn.commit() 419 | return self.c.lastrowid 420 | 421 | def convert_unix_to_date(self,unix_timestamp): 422 | """ 423 | Convert a Unix timestamp to a datetime object. 424 | """ 425 | timestamp_str = str(unix_timestamp) 426 | if len(timestamp_str) > 10: 427 | return datetime.fromtimestamp(round(unix_timestamp / 1000)) 428 | 429 | return datetime.fromtimestamp(unix_timestamp) 430 | 431 | 432 | async def update_token_account(self, wallet_address, wallet_token_account, block_time): 433 | try: 434 | wallet_address_id = self.get_wallet_address_id(str(wallet_address)) 435 | if wallet_address_id is None: 436 | print("Wallet address not found in the database.") 437 | return 438 | 439 | # Convert the Pubkey to a string 440 | wallet_token_account_str = str(wallet_token_account) # Adjust this line as necessary 441 | 442 | # Check if the token account already exists for this wallet address 443 | self.c.execute('SELECT * FROM token_accounts WHERE wallet_address_id = ? AND wallet_token_account = ?', 444 | (wallet_address_id, wallet_token_account_str)) 445 | result = self.c.fetchone() 446 | 447 | if not result: 448 | # Insert a new token account only if it does not exist 449 | self.c.execute( 450 | 'INSERT INTO token_accounts (wallet_address_id, wallet_token_account, block_time) VALUES (?, ?, ?)', 451 | (wallet_address_id, wallet_token_account_str, block_time)) 452 | # Commit the changes 453 | self.conn.commit() 454 | print(f"{style.CYAN}New token account added for wallet address: {str(wallet_address)}, token account: {wallet_token_account_str}",style.RESET) 455 | print("Calculating and updating pnl") 456 | print(wallet_token_account) 457 | await self.process_token_account(wallet_token_account,wallet_address_id) 458 | else: 459 | print("Account already exists") 460 | 461 | except Exception as e: 462 | print(f"Error updating token account: {e}") 463 | self.conn.rollback() 464 | 465 | 466 | 467 | def token_account_exists(self,wallet_address, wallet_token_account): 468 | wallet_address_id = self.get_wallet_address_id(str(wallet_address)) 469 | if wallet_address_id is None: 470 | print("Wallet address not found in the database.") 471 | return False 472 | 473 | 474 | self.c.execute('SELECT * FROM token_accounts WHERE wallet_address_id = ? AND wallet_token_account = ?', 475 | (wallet_address_id, str(wallet_token_account))) 476 | result = self.c.fetchone() 477 | 478 | if result: 479 | return True 480 | else: 481 | return False 482 | 483 | async def get_new_token_accounts(self,wallet_address: Pubkey): 484 | owner = self.wallet_address 485 | opts = TokenAccountOpts( 486 | program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") 487 | ) 488 | response = await self.async_solana_client.get_token_accounts_by_owner(owner, opts) 489 | number_tokenAccounts = len(response.value) 490 | all_token_accounts = response.value 491 | new_token_accounts = [] 492 | for token_account in all_token_accounts: 493 | sig= await self.async_solana_client.get_signatures_for_address(token_account.pubkey, limit=100) 494 | last_signature = await self.async_solana_client.get_transaction(sig.value[-1].signature, encoding="jsonParsed", 495 | max_supported_transaction_version=0) 496 | 497 | block_time=last_signature.value.block_time 498 | account_exist = self.token_account_exists(wallet_address, str(token_account.pubkey)) 499 | if not account_exist: 500 | new_token_accounts.append(token_account) 501 | await self.update_token_account(wallet_address, token_account.pubkey, block_time) 502 | else: 503 | print("Account already exists") 504 | print(f"{style.GREEN}Added all token account of {wallet_address} to the database",style.RESET) 505 | print("------------Generating Report---------------") 506 | reports_folder = "automate_processing_reports" 507 | summary_30_days = self.get_summary(30) 508 | win_rate_30_days = summary_30_days['WinRate'] if summary_30_days else 'Unknown' 509 | 510 | for time_period in [90, 60, 30, 14, 7]: 511 | summary = self.get_summary(time_period) 512 | transactions = self.get_transactions(time_period) 513 | 514 | if summary: 515 | # Create a new folder for each wallet address 516 | wallet_folder = os.path.join(reports_folder, f"{win_rate_30_days}_{self.wallet_address}") 517 | if not os.path.exists(wallet_folder): 518 | os.makedirs(wallet_folder) 519 | file_name = os.path.join(wallet_folder, f"{self.wallet_address}_{time_period}_days.xlsx") 520 | self.export_to_excel(summary, transactions, file_name) 521 | print(f"Exported summary and transactions for {time_period} days to {file_name}") 522 | else: 523 | print(f"No summary found for {time_period} days.") 524 | 525 | return new_token_accounts 526 | 527 | ### PNL INFO#### 528 | def get_token_data(self,decimals): 529 | for token_balances in decimals: 530 | if token_balances.owner == self.wallet_address and token_balances.mint != Pubkey.from_string(WRAPPED_SOL_MINT): 531 | token_contract = token_balances.mint 532 | token_decimal = token_balances.ui_token_amount.decimals 533 | return token_contract, token_decimal 534 | async def transactionType(self,Account:str): 535 | data_response = await self.async_solana_client.get_account_info(Pubkey.from_string(Account)) 536 | data = data_response.value.data 537 | parsed_data = layouts.SPL_ACCOUNT_LAYOUT.parse(data) 538 | mint = Pubkey.from_bytes(parsed_data.mint) 539 | if mint == WRAPPED_SOL_MINT: 540 | return mint 541 | return mint 542 | 543 | async def transactionDetails(self , txn: GetTransactionResp,transaction_array:list): 544 | transaction= txn 545 | information_array = transaction_array 546 | block_time = transaction.value.block_time 547 | txn_fee = transaction.value.transaction.meta.fee 548 | mint_decimal = self.mint_decimal 549 | mint_address=self.mint_address 550 | pre_tokenBalance = transaction.value.transaction.meta.pre_token_balances 551 | post_tokenBalance = transaction.value.transaction.meta.post_token_balances 552 | tokenAmount_Sold = pre_tokenBalance[-1].ui_token_amount.ui_amount 553 | tokenAuthority = pre_tokenBalance[-1].owner 554 | jupyter_transaction={} 555 | 556 | print(style.RED + "Length of inforamtion araay", len(information_array), style.RESET) 557 | 558 | if len(information_array)==2: 559 | 560 | if len(pre_tokenBalance) > 0: 561 | 562 | if pre_tokenBalance[0].mint != Pubkey.from_string("So11111111111111111111111111111111111111112"): 563 | mint_address = pre_tokenBalance[0].mint 564 | mint_decimal = pre_tokenBalance[0].ui_token_amount.decimals 565 | print(mint_decimal,mint_address) 566 | tokenAmount_Bought = post_tokenBalance[0].ui_token_amount.ui_amount 567 | else: 568 | mint_address = pre_tokenBalance[1].mint 569 | mint_decimal = pre_tokenBalance[1].ui_token_amount.decimals 570 | print(mint_decimal,mint_address) 571 | else: 572 | print("This is a transfer Continue") 573 | 574 | try: 575 | 576 | first_info = information_array[0] 577 | second_info = information_array[1] 578 | first_authority = first_info['authority'] 579 | transfer_type= await self.transactionType(second_info['source']) 580 | token_sold = int(first_info['amount']) / 10 ** mint_decimal 581 | sol_sold = int(second_info['amount']) / 10 ** 9 582 | sol_spent = int(first_info['amount']) / 10 ** 9 583 | token_bought = int(second_info['amount']) / 10 ** mint_decimal 584 | 585 | if first_authority== str(self.wallet_address) and str(transfer_type) != str(WRAPPED_SOL_MINT): 586 | self.update_buy(token_bought, txn_fee, block_time) 587 | self.spent_sol += sol_spent 588 | self.contract = mint_address 589 | self.last_trade = block_time 590 | print(f"{style.GREEN}BUY {sol_spent} SOL {style.RESET} -FOR {token_bought} TokenBought= {mint_address}") 591 | else: 592 | self.update_sell(token_sold, txn_fee, block_time) 593 | self.earned_sol += sol_sold 594 | self.contract = mint_address 595 | self.last_trade = block_time 596 | print(f"{style.RED}SELL {int(token_sold)} Token {style.RESET} -FOR {sol_sold} SOL TokenSold= {mint_address}") 597 | 598 | 599 | except Exception as e: 600 | print("Error",e) 601 | 602 | else: 603 | 604 | try: 605 | 606 | jupyter_transaction['first'] = information_array[0] 607 | jupyter_transaction['last'] = information_array[-1] 608 | 609 | if str(jupyter_transaction['last']['mint']) != str(WRAPPED_SOL_MINT) and str(jupyter_transaction['last']['mint']) != "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": 610 | 611 | print(style.YELLOW + "This is jupyter BUYYY", style.RESET) 612 | from_wallet = jupyter_transaction['first']['authority'] 613 | to_wallet = jupyter_transaction['last']['authority'] 614 | buy_amount = jupyter_transaction['first']['tokenAmount']['uiAmount'] 615 | mint = jupyter_transaction['last']['mint'] 616 | tokenAmount_Bought = jupyter_transaction['last']['tokenAmount']['uiAmount'] 617 | 618 | self.update_buy(tokenAmount_Bought, txn_fee, block_time) 619 | self.spent_sol += buy_amount 620 | self.contract = mint_address 621 | self.last_trade = block_time 622 | print(f"Buy amount {buy_amount}SOL for Token= {mint} Token Amount Bought= {tokenAmount_Bought}") 623 | jupyter_transaction={} 624 | else: 625 | print(style.BLUE + "This is jupyter SELL", style.RESET) 626 | from_wallet = jupyter_transaction['first']['authority'] 627 | to_wallet = jupyter_transaction['last']['authority'] 628 | tokenAmount_Sold = jupyter_transaction['first']['tokenAmount']['uiAmount'] 629 | mint = jupyter_transaction['first']['mint'] 630 | Soll_sell_amount = jupyter_transaction['last']['tokenAmount']['uiAmount'] 631 | self.update_sell(tokenAmount_Sold, txn_fee, block_time) 632 | self.earned_sol += Soll_sell_amount 633 | self.contract = mint 634 | self.last_trade = block_time 635 | print(f"Sell amount {tokenAmount_Sold} Token= {mint} for SOL= {Soll_sell_amount}") 636 | jupyter_transaction = {} 637 | except Exception as e: 638 | print("Jupyter Error", e) 639 | 640 | 641 | async def process_token_account(self, token_account: Pubkey,wallet_address_id: int): 642 | transaction_data_dict = {} 643 | error_count = 0 644 | self.reset_variables() 645 | token_account_str = str(token_account) 646 | 647 | # Directly process the transactions for the given token account 648 | sig = await self.async_solana_client.get_signatures_for_address(token_account, limit=500) 649 | valid=0 650 | # information_array = [] 651 | for signature in reversed(sig.value): 652 | if signature.err == None: 653 | transaction = await self.async_solana_client.get_transaction(signature.signature, encoding="jsonParsed", 654 | max_supported_transaction_version=0) 655 | txn_fee = transaction.value.transaction.meta.fee 656 | instruction_list = transaction.value.transaction.meta.inner_instructions 657 | account_signer = transaction.value.transaction.transaction.message.account_keys[0].pubkey 658 | decimals = transaction.value.transaction.meta.post_token_balances 659 | information_array = [] 660 | 661 | print(style.RED,signature.signature,style.RESET) 662 | for ui_inner_instructions in instruction_list: 663 | for txn_instructions in ui_inner_instructions.instructions: 664 | if txn_instructions.program_id == TOKEN_PROGRAM_ID: 665 | txn_information = txn_instructions.parsed['info'] 666 | if 'destination' in txn_information: 667 | information_array.append(txn_information) 668 | 669 | 670 | if account_signer == self.wallet_address: 671 | 672 | valid+=1 673 | 674 | try: 675 | if valid>0: 676 | await self.transactionDetails(transaction,information_array) 677 | 678 | print(style.CYAN,"Transaction Details Added",style.RESET) 679 | valid=0 680 | else: 681 | pass 682 | except Exception as e: 683 | print(e, signature.signature) 684 | print(style.RED,"Error Adding Transaction Details",style.RESET) 685 | continue 686 | 687 | await self.calculate_deltas() 688 | self.print_summary() 689 | 690 | await self.fill_pnl_info_table(token_account_str,wallet_address_id) 691 | 692 | 693 | 694 | 695 | async def pair_createdTime(self,token_traded): 696 | url = f'https://api.dexscreener.com/latest/dex/tokens/{token_traded}' 697 | response = requests.get(url) 698 | data = response.json() 699 | if data['pairs'] is not None: 700 | return data['pairs'][0]['pairCreatedAt'] 701 | return 0 702 | 703 | 704 | def calculate_time_difference(self,unix_timestamp1, unix_timestamp2): 705 | """ 706 | Calculate the difference between two Unix timestamps and return it in a human-readable format. 707 | """ 708 | date1 = self.convert_unix_to_date(unix_timestamp1) 709 | date2 = self.convert_unix_to_date(unix_timestamp2) 710 | 711 | # Calculate the difference between the two dates 712 | difference = date2 - date1 713 | 714 | # Calculate the difference in hours, minutes, and seconds 715 | hours, remainder = divmod(difference.total_seconds(), 3600) 716 | minutes, seconds = divmod(remainder, 60) 717 | 718 | # Format the output based on the difference 719 | if hours > 0: 720 | return f"{int(hours)}h {int(minutes)}m {int(seconds)}s" 721 | elif minutes > 0: 722 | return f"{int(minutes)}m {int(seconds)}s" 723 | else: 724 | return f"{int(seconds)}s" 725 | def update_buy(self, amount, fee, block_time): 726 | 727 | self.income += amount 728 | self.fee += fee 729 | self.buys += 1 730 | if self.first_buy_time is None: 731 | self.first_buy_time = block_time 732 | if self.tokenCreationTime != 0: 733 | self.buy_period= self.calculate_time_difference(self.first_buy_time, self.tokenCreationTime) 734 | else: 735 | self.buy_period="Unknown" 736 | 737 | 738 | 739 | def update_sell(self, amount, fee, block_time): 740 | self.outcome += amount 741 | self.fee += fee 742 | self.sells += 1 743 | self.last_sell_time = block_time 744 | if self.last_trade is None or block_time > self.last_trade: 745 | self.last_trade = block_time 746 | def print_summary(self): 747 | print(f"Income: {self.income}") 748 | print(f"Outcome: {self.outcome}") 749 | print(f"Total Fee: {self.fee/10**9}") 750 | print(f"Spent SOL: {self.spent_sol}") 751 | print(f"Earned SOL: {self.earned_sol}") 752 | print(f"Delta Token: {self.delta_token}") 753 | print(f"Delta SOL: {self.delta_sol}") 754 | print(f"Delta Percentage: {self.delta_percentage}%") 755 | print(f"Buys: {self.buys}") 756 | print(f"Sells: {self.sells}") 757 | print(f"Last Trade:{self.convert_unix_to_date(self.last_trade)}")###Check if this conversion is the problem 758 | print(f"Time Period: {self.time_period}") 759 | print(f"Contract: {self.contract}") 760 | print(f"Scam Tokens: {self.scam_tokens}") 761 | print(self.tokenCreationTime, self.first_buy_time) 762 | print("Buy Period: ",self.buy_period) 763 | 764 | 765 | 766 | def reset_variables(self): 767 | self.income = 0 768 | self.outcome = 0 769 | self.fee = 0 770 | self.spent_sol = 0 771 | self.earned_sol = 0 772 | self.buys = 0 773 | self.sells = 0 774 | self.first_buy_time = None 775 | self.last_sell_time = None 776 | self.last_trade = None 777 | self.time_period = 0 778 | self.contract = None 779 | self.scam_tokens = 0 780 | self.buy_period=0 781 | self.tokenCreationTime = 0 782 | self.mint_decimal=None 783 | 784 | 785 | async def getToken_SolAmount(self,): 786 | token_address = str(self.contract) 787 | url = f'https://api.dexscreener.com/latest/dex/tokens/{token_address}' 788 | response = requests.get(url) 789 | data = response.json() 790 | if data['pairs'] is not None: 791 | token_price_usd = float(data['pairs'][0]['priceUsd']) 792 | wallet_amount= self.income 793 | Worth_wallet_amount = token_price_usd * wallet_amount 794 | worth_in_solana= Worth_wallet_amount / self.solana_price 795 | return worth_in_solana 796 | else: 797 | self.scam_tokens = 1 798 | return 0 799 | 800 | 801 | 802 | 803 | 804 | async def calculate_deltas(self): 805 | 806 | if self.sells >=1: 807 | self.delta_token = self.income - self.outcome 808 | self.delta_sol = self.earned_sol - self.spent_sol 809 | self.delta_percentage = (self.delta_sol / self.spent_sol) * 100 if self.spent_sol != 0 else 0 810 | else: 811 | self.delta_token= self.income 812 | self.delta_sol = self.earned_sol-self.spent_sol 813 | self.delta_percentage= -100 814 | self.tokenCreationTime = await self.pair_createdTime(self.contract) 815 | if self.tokenCreationTime == 0: 816 | self.buy_period = "Unknown" 817 | 818 | else: 819 | self.buy_period = self.calculate_time_difference(self.tokenCreationTime, self.first_buy_time) 820 | 821 | # Calculate the time period 822 | if self.first_buy_time and self.last_sell_time: 823 | time_difference = self.last_sell_time - self.first_buy_time 824 | self.time_period = self.calculate_time_difference(self.first_buy_time, self.last_sell_time) 825 | elif self.first_buy_time and not self.last_sell_time: 826 | self.time_period = 0 # No sell, indicating a potential scam 827 | self.scam_tokens = 1 828 | 829 | 830 | async def fill_pnl_info_table(self, token_account,wallet_address_id): 831 | """ 832 | Insert a new PNL info only if the transaction is not yet in the database. 833 | """ 834 | 835 | fields = [ 836 | 837 | ('token_account', token_account), 838 | ('income', self.income), 839 | ('outcome', self.outcome), 840 | ('fee', self.fee), 841 | ('spent_sol', self.spent_sol), 842 | ('earned_sol', self.earned_sol), 843 | ('delta_token', self.delta_token), 844 | ('delta_sol', self.delta_sol), 845 | ('buys', self.buys), 846 | ('sells', self.sells), 847 | ('time_period', self.time_period), 848 | ('contract', self.contract), 849 | ('scam_tokens', self.scam_tokens), 850 | ('wallet_address_id', wallet_address_id), 851 | ('last_trade', self.last_trade), 852 | ('buy_period',self.buy_period) 853 | ] 854 | 855 | none_fields = [field_name for field_name, field_value in fields if field_value is None] 856 | if none_fields: 857 | print(f"One or more fields are None: {', '.join(none_fields)}. Skipping the operation.") 858 | return 859 | # Prepare the SQL INSERT statement 860 | insert_sql = ''' 861 | INSERT INTO pnl_info ( 862 | wallet_address_id, token_account, income, outcome, total_fee, spent_sol, earned_sol, 863 | delta_token, delta_sol, delta_percentage, buys, sells, last_trade, 864 | time_period, contract, scam_tokens,buy_period 865 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?) 866 | ''' 867 | 868 | if any(value is None for value in 869 | [token_account, self.income, self.outcome, self.fee, self.spent_sol, self.earned_sol, self.delta_token, 870 | self.delta_sol, self.buys, self.sells, self.time_period, self.contract, self.scam_tokens, 871 | wallet_address_id, self.last_trade, self.calculate_time_difference(self.first_buy_time, self.tokenCreationTime) if self.tokenCreationTime != 0 or self.first_buy_time!= None else 0]): 872 | print("One or more fields are None. Skipping the operation.") 873 | return 874 | 875 | # Check if the transaction already exists in the database 876 | check_sql = ''' 877 | SELECT 1 FROM pnl_info 878 | WHERE wallet_address_id = ? AND last_trade = ? 879 | ''' 880 | cursor = self.conn.cursor() 881 | cursor.execute(check_sql, (wallet_address_id, self.last_trade)) 882 | if cursor.fetchone() is not None: 883 | print("Transaction already exists in the database. Skipping the operation.") 884 | return 885 | 886 | insert_data = ( 887 | wallet_address_id, 888 | token_account, # Use the token_account parameter 889 | self.income, 890 | self.outcome, 891 | self.fee / 10 ** 9, 892 | self.spent_sol, 893 | self.earned_sol, 894 | self.delta_token, 895 | self.delta_sol, 896 | self.delta_percentage, 897 | self.buys, 898 | self.sells, 899 | self.last_trade, 900 | self.time_period, 901 | str(self.contract), 902 | self.scam_tokens, 903 | self.buy_period 904 | ) 905 | 906 | # Insert the new row 907 | cursor.execute(insert_sql, insert_data) 908 | self.conn.commit() 909 | print("PNL info successfully inserted into the database.") 910 | 911 | async def process_transactions(self): 912 | while True: 913 | signature = await self.queue.get() 914 | try: 915 | transaction = await self.async_solana_client.get_transaction(signature, encoding="jsonParsed", 916 | max_supported_transaction_version=0) 917 | instruction_list = transaction.value.transaction.meta.inner_instructions 918 | accounts = transaction.value.transaction.transaction.message.account_keys 919 | if accounts[-1].pubkey == Pubkey.from_string(Pool_raydium): 920 | self.wallet_address = accounts[0].pubkey 921 | await self.initialize() 922 | 923 | self.wallet_address_id = self.get_wallet_address_id(self.wallet_address) 924 | num_tokenAccounts = await self.get_token_accountsCount(self.wallet_address) 925 | print("Number of token Account", self.wallet_address, num_tokenAccounts) 926 | if num_tokenAccounts in range(1, int(config['max_token_accounts']) + 1): 927 | await self.get_new_token_accounts(self.wallet_address) 928 | 929 | 930 | except Exception as e: 931 | print(f"Failed to process transaction {signature}: {e}") 932 | pass 933 | finally: 934 | self.queue.task_done() 935 | 936 | async def enqueue_transaction(self, signature): 937 | await self.queue.put(signature) 938 | 939 | 940 | async def run(): 941 | processor = TransactionProcessor() 942 | asyncio.create_task(processor.process_transactions()) 943 | # await asyncio.gather( 944 | # # processor.process_transactions(), 945 | # processor.generate_reports_for_time_periods("9KupEacYuc5Pt7A8nhYnFLnwFFsVR9othS7azjV8rqzP",[365]), 946 | # # processor.get_summary(1), 947 | # processor.initialize() 948 | # ) 949 | 950 | while True: # Loop for reconnection attempts 951 | try: 952 | 953 | uri = "wss://mainnet.helius-rpc.com/?api-key=API_KEY" #Enter your api key 954 | async with websockets.connect(uri, ping_timeout=30) as websocket: 955 | await websocket.send(json.dumps({ 956 | "jsonrpc": "2.0", 957 | "id": 1, 958 | "method": "logsSubscribe", 959 | "params": [ 960 | {"mentions": [config['automated_wallet_address']]}, 961 | {"commitment": "finalized"} 962 | ] 963 | })) 964 | 965 | first_resp = await websocket.recv() 966 | response_dict = json.loads(first_resp) 967 | if 'result' in response_dict: 968 | print("Subscription successful. Subscription ID: ", response_dict['result']) 969 | async for response in websocket: 970 | response_dict = json.loads(response) 971 | if response_dict['params']['result']['value']['err'] is None: 972 | signature = response_dict['params']['result']['value']['signature'] 973 | if signature not in seen_signatures: 974 | seen_signatures.add(signature) 975 | await processor.enqueue_transaction(Signature.from_string(signature)) 976 | 977 | except websockets.exceptions.ConnectionClosedError as e: 978 | print(f"Connection closed with error: {e}. Reconnecting in 5 seconds...") 979 | await asyncio.sleep(5) 980 | except Exception as e: 981 | print(f"An unexpected error occurred: {e}. Attempting reconnect in 5 seconds...") 982 | await asyncio.sleep(5) 983 | 984 | 985 | asyncio.run(run()) -------------------------------------------------------------------------------- /Manual.py: -------------------------------------------------------------------------------- 1 | import os 2 | # import sys 3 | import pandas as pd 4 | from solders.rpc.responses import GetTransactionResp 5 | import layouts 6 | import asyncio 7 | import datetime 8 | import requests 9 | from solders.pubkey import Pubkey 10 | from solana.rpc.async_api import AsyncClient 11 | from datetime import datetime 12 | from spl.token.constants import TOKEN_PROGRAM_ID,WRAPPED_SOL_MINT 13 | 14 | from solana.rpc import types 15 | from solana.rpc.types import TokenAccountOpts 16 | # from openpyxl import Workbook 17 | from openpyxl.styles import Font, Alignment, PatternFill, Border, Side 18 | from openpyxl.utils import get_column_letter 19 | 20 | from dotenv import dotenv_values 21 | config = dotenv_values(".env") 22 | 23 | 24 | class style(): 25 | BLACK = '\033[30m' 26 | RED = '\033[31m' 27 | GREEN = '\033[32m' 28 | YELLOW = '\033[33m' 29 | BLUE = '\033[34m' 30 | MAGENTA = '\033[35m' 31 | CYAN = '\033[36m' 32 | WHITE = '\033[37m' 33 | UNDERLINE = '\033[4m' 34 | RESET = '\033[0m' 35 | 36 | seen_signatures = set() 37 | 38 | import sqlite3 39 | class TransactionProcessor: 40 | def __init__(self,wallet_address): 41 | self.queue = asyncio.Queue() 42 | self.wallet_address = wallet_address 43 | 44 | # self.seen_signatures = set() 45 | self.async_solana_client = AsyncClient(config["RPC_HTTPS_URL"]) 46 | 47 | self.db_name = "testing.db" 48 | 49 | self.conn = sqlite3.connect(self.db_name) 50 | 51 | self.c = self.conn.cursor() 52 | self.init_db() 53 | self.income = 0 54 | self.outcome = 0 55 | self.fee = 0 56 | self.spent_sol = 0 57 | self.earned_sol = 0 58 | self.buys = 0 59 | self.sells = 0 60 | self.delta_sol=0 61 | self.delta_token=0 62 | self.delta_percentage=0 63 | self.first_buy_time = None 64 | self.last_sell_time = None 65 | self.last_trade = None 66 | self.time_period = 0 67 | 68 | self.contract = None 69 | self.scam_tokens = 0 70 | 71 | # self.sol_balance = self.getSOlBalance() 72 | self.sol_balance = None 73 | self.solana_price = self.get_current_solana_price() 74 | self.tokenCreationTime = None 75 | self.solAccount= None 76 | self.mint_decimal=None 77 | self.mint_address=None 78 | self.buy_period = 0 79 | 80 | 81 | def init_db(self): 82 | self.c.execute(''' 83 | CREATE TABLE IF NOT EXISTS wallet_address ( 84 | id INTEGER PRIMARY KEY AUTOINCREMENT, 85 | wallet_address TEXT UNIQUE 86 | ) 87 | ''') 88 | 89 | 90 | self.c.execute(''' 91 | CREATE TABLE IF NOT EXISTS token_accounts ( 92 | wallet_address_id INTEGER, 93 | wallet_token_account TEXT, 94 | block_time INTEGER, 95 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 96 | ) 97 | ''') 98 | 99 | self.c.execute(''' 100 | CREATE TABLE IF NOT EXISTS pnl_info ( 101 | token_account TEXT PRIMARY KEY, 102 | wallet_address_id INTEGER, 103 | income REAL, 104 | outcome REAL, 105 | total_fee REAL, 106 | spent_sol REAL, 107 | earned_sol REAL, 108 | delta_token REAL, 109 | delta_sol REAL, 110 | delta_percentage REAL, 111 | buys INTEGER, 112 | sells INTEGER, 113 | last_trade TEXT, 114 | time_period TEXT, 115 | contract TEXT, 116 | scam_tokens TEXT, 117 | buy_period TEXT, 118 | 119 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 120 | ) 121 | ''') 122 | 123 | 124 | self.c.execute(''' 125 | CREATE TABLE IF NOT EXISTS winning_wallets ( 126 | wallet_address_id INTEGER, 127 | win_rate_7 REAL, 128 | balance_change_7 REAL, 129 | token_accounts_7 INTEGER, 130 | win_rate_14 REAL, 131 | balance_change_14 REAL, 132 | token_accounts_14 INTEGER, 133 | win_rate_30 REAL, 134 | balance_change_30 REAL, 135 | token_accounts_30 INTEGER, 136 | win_rate_60 REAL, 137 | balance_change_60 REAL, 138 | token_accounts_60 INTEGER, 139 | win_rate_90 REAL, 140 | balance_change_90 REAL, 141 | token_accounts_90 INTEGER, 142 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 143 | ) 144 | ''') 145 | self.conn.commit() 146 | 147 | async def get_token_accountsCount(self,wallet_address: Pubkey): 148 | owner = wallet_address 149 | opts = types.TokenAccountOpts(program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) 150 | response = await self.async_solana_client.get_token_accounts_by_owner(owner, opts) 151 | # length= len(response.value) 152 | return len(response.value) 153 | 154 | 155 | async def initialize(self): 156 | self.sol_balance = await self.getSOlBalance() 157 | 158 | 159 | ##REPORTING### 160 | async def getSOlBalance(self): 161 | pubkey = self.wallet_address 162 | response = await self.async_solana_client.get_balance(pubkey) 163 | balance = response.value / 10**9 164 | return balance 165 | 166 | def get_current_solana_price(self): 167 | url = "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd" 168 | response = requests.get(url) 169 | data = response.json() 170 | return data['solana']['usd'] if response.status_code == 200 else None 171 | 172 | 173 | def store_win_rate(self, time_period, win_rate, balance_change, token_accounts): 174 | # First, check if a row with the given wallet_address_id exists 175 | check_sql = 'SELECT 1 FROM winning_wallets WHERE wallet_address_id = ?' 176 | self.c.execute(check_sql, (self.wallet_address_id,)) 177 | row_exists = self.c.fetchone() is not None 178 | 179 | time_period_suffix = f"_{time_period}" 180 | 181 | if row_exists: 182 | # If the row exists, update the win_rate, balance_change, and token_accounts for the specified time_period 183 | update_sql = f''' 184 | UPDATE winning_wallets 185 | SET win_rate{time_period_suffix} = ?, 186 | balance_change{time_period_suffix} = ?, 187 | token_accounts{time_period_suffix} = ? 188 | WHERE wallet_address_id = ? 189 | ''' 190 | self.c.execute(update_sql, (win_rate, balance_change, token_accounts, self.wallet_address_id)) 191 | else: 192 | # If the row does not exist, insert a new row with default values set to NULL or 0 and then update 193 | # the specific time period data 194 | insert_sql = f''' 195 | INSERT INTO winning_wallets ( 196 | wallet_address_id, 197 | win_rate_7, balance_change_7, token_accounts_7, 198 | win_rate_14, balance_change_14, token_accounts_14, 199 | win_rate_30, balance_change_30, token_accounts_30, 200 | win_rate_60, balance_change_60, token_accounts_60, 201 | win_rate_90, balance_change_90, token_accounts_90 202 | ) VALUES ( 203 | ?, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL 204 | ) 205 | ''' 206 | self.c.execute(insert_sql, (self.wallet_address_id,)) 207 | # Now the row exists, update the specific time period data 208 | update_sql = f''' 209 | UPDATE winning_wallets 210 | SET win_rate{time_period_suffix} = ?, 211 | balance_change{time_period_suffix} = ?, 212 | token_accounts{time_period_suffix} = ? 213 | WHERE wallet_address_id = ? 214 | ''' 215 | self.c.execute(update_sql, (win_rate, balance_change, token_accounts, self.wallet_address_id)) 216 | 217 | # Commit the changes 218 | self.conn.commit() 219 | 220 | def get_summary(self, time_period): 221 | query = f''' 222 | WITH Calculations AS ( 223 | SELECT 224 | (SUM(CASE WHEN delta_sol > 0 THEN 1 ELSE 0 END) * 1.0 / COUNT(*)) * 100 AS WinRate, 225 | -- SUM(delta_sol) AS PnL_R, 226 | -- SUM(CASE WHEN delta_sol < 0 THEN delta_sol ELSE 0 END) AS PnL_Loss, 227 | SUM(CASE WHEN delta_sol > 0 THEN delta_sol ELSE 0 END) AS PnL_R, -- Changed line 228 | SUM(CASE WHEN delta_sol < 0 THEN delta_sol ELSE 0 END) AS PnL_Loss, 229 | (SUM(earned_sol) / NULLIF(SUM(spent_sol), 0) - 1) * 100 AS Balance_Change, 230 | COUNT(CASE WHEN scam_tokens = 1 THEN 1 END) AS ScamTokens, 231 | COUNT(token_account) AS TokenAccounts 232 | FROM pnl_info 233 | WHERE wallet_address_id = ? 234 | AND last_trade >= strftime('%s', 'now', '-{time_period} days') 235 | ) 236 | SELECT 237 | *, 238 | '{time_period} days' AS TimePeriod 239 | FROM Calculations; 240 | ''' 241 | 242 | self.c.execute(query, (self.get_wallet_address_id(self.wallet_address),)) 243 | 244 | summary_result = self.c.fetchone() 245 | win_rate = summary_result[0] 246 | balance_change = summary_result[3] 247 | count_token_accounts=summary_result[5] 248 | 249 | # Store the win rate in the 'winning_wallets' table 250 | self.store_win_rate(time_period, win_rate,balance_change,count_token_accounts) 251 | 252 | 253 | 254 | summary_data = { 255 | 'SolBalance': self.sol_balance, 256 | 'WalletAddress': str(self.wallet_address), 257 | 'WinRate': summary_result[0], 258 | 'PnL_R': summary_result[1], 259 | 'PnL_Loss': summary_result[2], 260 | 'Balance_Change': summary_result[3], 261 | 'ScamTokens': summary_result[4], 262 | 'TimePeriod': summary_result[6] # 263 | } 264 | # print(summary_data) 265 | 266 | return summary_data 267 | 268 | 269 | def get_transactions(self, time_period): 270 | query = f''' 271 | SELECT * 272 | FROM pnl_info 273 | WHERE wallet_address_id = ? 274 | AND last_trade >= strftime('%s', 'now', '-{time_period} days'); 275 | ''' 276 | self.c.execute(query, (self.wallet_address_id,)) 277 | results = self.c.fetchall() 278 | # print(results) 279 | #return results 280 | transactions_df = pd.DataFrame(results, columns=['token_account', 'wallet_address_id', 'income', 'outcome', 281 | 'total_fee', 'spent_sol', 'earned_sol', 'delta_token', 282 | 'delta_sol', 'delta_percentage', 'buys', 'sells', 283 | 'last_trade', 'time_period', 'contract', 'scam token','buy_period']) 284 | 285 | # Sort transactions by 'last_trade' in descending order 286 | transactions_df = transactions_df.sort_values(by='last_trade', ascending=False) 287 | 288 | 289 | return transactions_df 290 | 291 | async def generate_reports_for_time_periods(self, time_periods): 292 | # Ensure initialization is done, for example, to fetch current SOL balance 293 | await self.initialize() 294 | 295 | # Fetch the wallet address ID from the database 296 | self.wallet_address_id = self.get_wallet_address_id(self.wallet_address) 297 | 298 | # Directory to store the reports 299 | reports_folder = "processing_reports" 300 | if not os.path.exists(reports_folder): 301 | os.makedirs(reports_folder) 302 | wallet_folder = os.path.join(reports_folder, str(self.wallet_address)) 303 | if not os.path.exists(wallet_folder): 304 | os.makedirs(wallet_folder) 305 | 306 | # Generate and export reports for each specified time period 307 | for time_period in time_periods: 308 | summary = self.get_summary(time_period) 309 | # print(time_period,summary) 310 | transactions = self.get_transactions(time_period) 311 | if summary: 312 | 313 | file_name = os.path.join(wallet_folder, f"{self.wallet_address}_{time_period}_days.xlsx") 314 | self.export_to_excel(summary, transactions, file_name) 315 | print(f"Exported summary and transactions for {time_period} days to {file_name}") 316 | else: 317 | print(f"No summary found for {time_period} days.") 318 | 319 | def export_to_excel(self,summary, transactions, file_name): 320 | sol_current_price = self.solana_price 321 | # print(transactions) 322 | summary['Current_Sol_Price'] = sol_current_price 323 | summary['ID'] = self.wallet_address_id 324 | win_rate =summary['WinRate'] 325 | 326 | try: 327 | summary['Profit_USD'] = summary['PnL_R'] * sol_current_price 328 | summary['Loss_USD'] = abs(summary['PnL_Loss']) * sol_current_price 329 | except TypeError: 330 | summary['Profit_USD'] = 0 331 | summary['Loss_USD'] = 0 332 | 333 | # Convert summary dictionary to DataFrame 334 | summary_df = pd.DataFrame([summary], columns=['ID','WalletAddress', 'SolBalance','Current_Sol_Price', 'WinRate', 'PnL_R', 'PnL_Loss', 335 | 'Balance_Change', 'ScamTokens', 'Profit_USD', 336 | 'Loss_USD', 'TimePeriod']) 337 | 338 | # Convert transactions to DataFrame 339 | transactions_df = pd.DataFrame(transactions, columns=['token_account','wallet_address_id', 'income', 'outcome', 340 | 'total_fee', 'spent_sol', 'earned_sol', 'delta_token', 341 | 'delta_sol', 'delta_percentage', 'buys', 'sells', 342 | 'last_trade', 'time_period','buy_period', 'contract', 'scam token']) 343 | transactions_df.drop(columns=['wallet_address_id'], inplace=True) 344 | transactions_df['last_trade'] = transactions_df['last_trade'].apply(lambda x: datetime.fromtimestamp(int(x)).strftime('%d.%m.%Y')) 345 | 346 | 347 | with pd.ExcelWriter(file_name, engine='openpyxl') as writer: 348 | 349 | # Write DataFrames to Excel 350 | summary_df.to_excel(writer, sheet_name='Summary and Transactions', index=False, startrow=0) 351 | row_to_start = len(summary_df) + 2 # Adjust row spacing 352 | transactions_df.to_excel(writer, sheet_name='Summary and Transactions', index=False, startrow=row_to_start) 353 | 354 | workbook = writer.book 355 | worksheet = writer.sheets['Summary and Transactions'] 356 | 357 | for row in worksheet.iter_rows(min_row=row_to_start + 2, max_col=worksheet.max_column): 358 | for cell in row: 359 | if cell.column == 1: # 'token_account' column 360 | cell.hyperlink = f'https://solscan.io/account/{cell.value}#splTransfer' 361 | cell.value = 'View Solscan' 362 | cell.font = Font(underline='single') 363 | elif cell.column == 15: # 'contract' column 364 | cell.hyperlink = f'https://dexscreener.com/solana/{cell.value}?maker={self.wallet_address}' 365 | 'https://dexscreener.com/solana/3zcoadmvqtx3itfthwr946nhznqh92eq9hdhhjtgp6as?maker=3uij3uDg5pLBBxQw6hUXxqw6uEwCQGX7rM8PZb7ofH9e' 366 | cell.value = 'View Dexscreener' 367 | cell.font = Font(underline='single') 368 | 369 | 370 | # Define fills for conditional formatting 371 | red_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") 372 | green_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid") 373 | yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") 374 | brown_fill = PatternFill(start_color="A52A2A", end_color="A52A2A", fill_type="solid") 375 | gold_fill = PatternFill(start_color="FFD700", end_color="FFD700", fill_type="solid") 376 | 377 | # Apply initial styling 378 | for row in worksheet.iter_rows(min_row=1, max_row=worksheet.max_row, min_col=1, 379 | max_col=worksheet.max_column): 380 | for cell in row: 381 | cell.border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), 382 | bottom=Side(style='thin')) 383 | cell.alignment = Alignment(horizontal="center", vertical="center") 384 | if cell.row == 1: 385 | cell.font = Font(bold=True) 386 | 387 | # Adjust column widths 388 | for column_cells in worksheet.columns: 389 | length = max(len(str(cell.value)) for cell in column_cells) 390 | worksheet.column_dimensions[get_column_letter(column_cells[0].column)].width = length + 2 391 | 392 | # Conditional formatting for transactions 393 | for idx, row in enumerate(transactions_df.itertuples(index=False), start=row_to_start + 2): 394 | outcome = row[2] 395 | income=row[1] 396 | delta_percentage = row[8] 397 | time_period = row[12] 398 | buys = row[9] 399 | if round(outcome,1) > round(income,1): 400 | worksheet.cell(row=idx, column=3).fill = yellow_fill 401 | 402 | 403 | if delta_percentage ==-100: 404 | 405 | worksheet.cell(row=idx, column=9).fill = brown_fill 406 | 407 | if pd.to_timedelta(time_period) < pd.Timedelta(minutes=1): 408 | worksheet.cell(row=idx, column=13).fill = yellow_fill 409 | 410 | if buys > 3: 411 | worksheet.cell(row=idx, column=10).fill = yellow_fill 412 | 413 | # Additional conditional formatting for delta_sol and delta_percentage 414 | # Assuming 'delta_sol' is the 9th column and 'delta_percentage' is the 10th column 415 | for idx, row in enumerate(transactions_df.itertuples(index=False), start=row_to_start + 2): 416 | # For delta_sol 417 | if row[7] < 0: 418 | worksheet.cell(row=idx, column=8).fill = red_fill 419 | elif row[7] > 0: 420 | worksheet.cell(row=idx, column=8).fill = green_fill 421 | 422 | # For delta_percentage 423 | 424 | if row[7] < 0 and row[8] != -100: 425 | worksheet.cell(row=idx, column=9).fill = red_fill 426 | elif row[8] > 0: 427 | worksheet.cell(row=idx, column=9).fill = green_fill 428 | 429 | 430 | for idx, row in enumerate(summary_df.itertuples(index=False), start=1): 431 | # print(row[9], row[10]) 432 | if row[9] > row[10] and row[4]>=50: # If Profit_USD > Loss_USD 433 | worksheet.cell(row=idx, column=10).fill = gold_fill 434 | worksheet.cell(row=idx, column=5).fill = gold_fill 435 | 436 | elif row[9] Loss_USD 437 | worksheet.cell(row=idx, column=10).fill = red_fill 438 | worksheet.cell(row=idx, column=5).fill = red_fill 439 | 440 | elif row[9] < row[10] and row[4] == 50: # If Profit_USD > Loss_USD 441 | worksheet.cell(row=idx, column=10).fill = red_fill 442 | worksheet.cell(row=idx, column=5).fill = gold_fill 443 | 444 | elif row[9] > row[10] and row[4] < 50: 445 | worksheet.cell(row=idx, column=10).fill = gold_fill 446 | worksheet.cell(row=idx, column=5).fill = red_fill 447 | else: 448 | worksheet.cell(row=idx, column=11).fill = red_fill 449 | worksheet.cell(row=idx, column=5).fill = red_fill 450 | 451 | 452 | 453 | 454 | # Save the workbook 455 | workbook.save(file_name) 456 | 457 | 458 | 459 | 460 | def get_wallet_address_id(self,wallet_address): 461 | # Check if the wallet address exists in the database 462 | self.c.execute('SELECT id FROM wallet_address WHERE wallet_address = ?', (str(wallet_address),)) 463 | result = self.c.fetchone() 464 | 465 | if result: 466 | # If the wallet address exists, return the ID 467 | return result[0] 468 | else: 469 | # If the wallet address does not exist, insert it into the database 470 | self.c.execute('INSERT INTO wallet_address (wallet_address) VALUES (?)', (str(wallet_address),)) 471 | # Commit the transaction to save the changes to the database 472 | self.conn.commit() 473 | # Return the ID of the newly inserted record 474 | return self.c.lastrowid 475 | 476 | def convert_unix_to_date(self,unix_timestamp): 477 | """ 478 | Convert a Unix timestamp to a datetime object. 479 | """ 480 | timestamp_str = str(unix_timestamp) 481 | # If the length is greater than 10 digits, it's likely in milliseconds 482 | if len(timestamp_str) > 10: 483 | print("retruned",datetime.fromtimestamp(round(unix_timestamp / 1000))) 484 | return datetime.fromtimestamp(round(unix_timestamp / 1000)) 485 | 486 | return datetime.fromtimestamp(unix_timestamp) 487 | 488 | 489 | 490 | async def update_token_account(self, wallet_address, wallet_token_account, block_time): 491 | try: 492 | wallet_address_id = self.get_wallet_address_id(str(wallet_address)) 493 | if wallet_address_id is None: 494 | print("Wallet address not found in the database.") 495 | return 496 | 497 | # Convert the Pubkey to a string 498 | wallet_token_account_str = str(wallet_token_account) # Adjust this line as necessary 499 | 500 | # Check if the token account already exists for this wallet address 501 | self.c.execute('SELECT * FROM token_accounts WHERE wallet_address_id = ? AND wallet_token_account = ?', 502 | (wallet_address_id, wallet_token_account_str)) 503 | result = self.c.fetchone() 504 | 505 | if not result: 506 | # Insert a new token account only if it does not exist 507 | self.c.execute( 508 | 'INSERT INTO token_accounts (wallet_address_id, wallet_token_account, block_time) VALUES (?, ?, ?)', 509 | (wallet_address_id, wallet_token_account_str, block_time)) 510 | # Commit the changes 511 | self.conn.commit() 512 | print(f"{style.CYAN}New token account added for wallet address: {str(wallet_address)}, token account: {wallet_token_account_str}",style.RESET) 513 | print("Calculating and updating pnl") 514 | print(wallet_token_account) 515 | await self.process_token_account(wallet_token_account,wallet_address_id) 516 | else: 517 | print("Account already exists") 518 | pass 519 | # await self.process_token_account(wallet_address) 520 | 521 | except Exception as e: 522 | print(f"Error updating token account: {e}") 523 | # Rollback the transaction in case of an error 524 | self.conn.rollback() 525 | 526 | 527 | 528 | def token_account_exists(self,wallet_address, wallet_token_account): 529 | wallet_address_id = self.get_wallet_address_id(str(wallet_address)) 530 | if wallet_address_id is None: 531 | print("Wallet address not found in the database.") 532 | return False 533 | 534 | 535 | self.c.execute('SELECT * FROM token_accounts WHERE wallet_address_id = ? AND wallet_token_account = ?', 536 | (wallet_address_id, str(wallet_token_account))) 537 | result = self.c.fetchone() 538 | 539 | if result: 540 | # The token account exists in the database for the given wallet address 541 | return True 542 | else: 543 | # The token account does not exist in the database for the given wallet address 544 | return False 545 | 546 | async def get_new_token_accounts(self,wallet_address: Pubkey): 547 | owner = self.wallet_address 548 | opts = TokenAccountOpts( 549 | program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") 550 | ) 551 | response = await self.async_solana_client.get_token_accounts_by_owner(owner, opts) 552 | number_tokenAccounts = len(response.value) 553 | all_token_accounts = response.value 554 | # print(len(all_token_accounts)) 555 | 556 | new_token_accounts = [] 557 | 558 | 559 | for token_account in all_token_accounts: 560 | sig= await self.async_solana_client.get_signatures_for_address(token_account.pubkey, limit=100) 561 | last_signature = await self.async_solana_client.get_transaction(sig.value[-1].signature, encoding="jsonParsed", 562 | max_supported_transaction_version=0) 563 | 564 | block_time=last_signature.value.block_time 565 | 566 | account_exist = self.token_account_exists(wallet_address, str(token_account.pubkey)) 567 | # print(account_exist) 568 | 569 | if not account_exist: 570 | new_token_accounts.append(token_account) 571 | await self.update_token_account(wallet_address, token_account.pubkey, block_time) 572 | 573 | 574 | else: 575 | print("Account already exists") 576 | # continue 577 | print(f"{style.GREEN}Added all token account of {wallet_address} to the database",style.RESET) 578 | print("------------Generating Report---------------") 579 | 580 | return new_token_accounts 581 | 582 | ### PNL INFO#### 583 | def get_token_data(self,decimals): 584 | for token_balances in decimals: 585 | if token_balances.owner == self.wallet_address and token_balances.mint != Pubkey.from_string(WRAPPED_SOL_MINT): 586 | token_contract = token_balances.mint 587 | token_decimal = token_balances.ui_token_amount.decimals 588 | return token_contract, token_decimal 589 | 590 | async def transactionType(self,Account:str): 591 | data_response = await self.async_solana_client.get_account_info(Pubkey.from_string(Account)) 592 | data = data_response.value.data 593 | parsed_data = layouts.SPL_ACCOUNT_LAYOUT.parse(data) 594 | mint = Pubkey.from_bytes(parsed_data.mint) 595 | # print(style.RED+"Mint",mint,style.RESET) 596 | if mint == WRAPPED_SOL_MINT: 597 | return mint 598 | return mint 599 | 600 | async def transactionDetails(self , txn: GetTransactionResp,transaction_array:list): 601 | transaction= txn 602 | information_array = transaction_array 603 | block_time = transaction.value.block_time 604 | txn_fee = transaction.value.transaction.meta.fee 605 | mint_decimal = self.mint_decimal 606 | mint_address=self.mint_address 607 | pre_tokenBalance = transaction.value.transaction.meta.pre_token_balances 608 | post_tokenBalance = transaction.value.transaction.meta.post_token_balances 609 | tokenAmount_Sold = pre_tokenBalance[-1].ui_token_amount.ui_amount 610 | tokenAuthority = pre_tokenBalance[-1].owner 611 | jupyter_transaction={} 612 | 613 | # print(style.RED + "Length of inforamtion araay", len(information_array), style.RESET) 614 | 615 | 616 | if len(information_array)==2: 617 | 618 | if len(pre_tokenBalance) > 0: 619 | 620 | if pre_tokenBalance[0].mint != Pubkey.from_string("So11111111111111111111111111111111111111112"): 621 | mint_address = pre_tokenBalance[0].mint 622 | mint_decimal = pre_tokenBalance[0].ui_token_amount.decimals 623 | print(mint_decimal,mint_address) 624 | tokenAmount_Bought = post_tokenBalance[0].ui_token_amount.ui_amount 625 | else: 626 | mint_address = pre_tokenBalance[1].mint 627 | mint_decimal = pre_tokenBalance[1].ui_token_amount.decimals 628 | print(mint_decimal,mint_address) 629 | else: 630 | print("This is a transfer Continue") 631 | 632 | try: 633 | 634 | first_info = information_array[0] 635 | second_info = information_array[1] 636 | first_authority = first_info['authority'] 637 | 638 | transfer_type= await self.transactionType(second_info['source']) 639 | 640 | token_sold = int(first_info['amount']) / 10 ** mint_decimal 641 | sol_sold = int(second_info['amount']) / 10 ** 9 642 | sol_spent = int(first_info['amount']) / 10 ** 9 643 | token_bought = int(second_info['amount']) / 10 ** mint_decimal 644 | 645 | if first_authority== str(self.wallet_address) and str(transfer_type) != str(WRAPPED_SOL_MINT): 646 | self.update_buy(token_bought, txn_fee, block_time) 647 | self.spent_sol += sol_spent 648 | self.contract = mint_address 649 | self.last_trade = block_time 650 | print(f"{style.GREEN}BUY {sol_spent} SOL {style.RESET} -FOR {token_bought} TokenBought= {mint_address}") 651 | else: 652 | self.update_sell(token_sold, txn_fee, block_time) 653 | self.earned_sol += sol_sold 654 | self.contract = mint_address 655 | self.last_trade = block_time 656 | print(f"{style.RED}SELL {int(token_sold)} Token {style.RESET} -FOR {style.GREEN} {sol_sold} SOL {style.RESET} TokenSold= {mint_address}") 657 | 658 | 659 | except Exception as e: 660 | print("Error",e) 661 | 662 | else: 663 | 664 | try: 665 | 666 | jupyter_transaction['first'] = information_array[0] 667 | 668 | # Add the second dictionary under another custom key 669 | jupyter_transaction['last'] = information_array[-1] 670 | 671 | 672 | if len(information_array) !=1: 673 | if str(jupyter_transaction['last']['mint']) != str(WRAPPED_SOL_MINT): 674 | 675 | print(style.YELLOW + "This is jupyter BUYYY", style.RESET) 676 | from_wallet = jupyter_transaction['first']['authority'] 677 | to_wallet = jupyter_transaction['last']['authority'] 678 | buy_amount = jupyter_transaction['first']['tokenAmount']['uiAmount'] 679 | mint = jupyter_transaction['last']['mint'] 680 | tokenAmount_Bought = jupyter_transaction['last']['tokenAmount']['uiAmount'] 681 | 682 | self.update_buy(tokenAmount_Bought, txn_fee, block_time) 683 | self.spent_sol += buy_amount 684 | self.contract = mint_address 685 | self.last_trade = block_time 686 | print(f"{style.GREEN}BUY {buy_amount} SOL {style.RESET} -FOR Token= {mint} TokenBought= {tokenAmount_Bought}") 687 | jupyter_transaction={} 688 | else: 689 | print(style.BLUE + "This is jupyter SELL", style.RESET) 690 | from_wallet = jupyter_transaction['first']['authority'] 691 | to_wallet = jupyter_transaction['last']['authority'] 692 | tokenAmount_Sold = jupyter_transaction['first']['tokenAmount']['uiAmount'] 693 | mint = jupyter_transaction['first']['mint'] 694 | Soll_sell_amount = jupyter_transaction['last']['tokenAmount']['uiAmount'] 695 | 696 | self.update_sell(tokenAmount_Sold, txn_fee, block_time) 697 | self.earned_sol += Soll_sell_amount 698 | self.contract = mint 699 | self.last_trade = block_time 700 | print(f"{style.RED}SELL {tokenAmount_Sold} Token {style.RESET} -FOR {style.GREEN}{Soll_sell_amount} {style.RESET}SOL TokenSold= {mint}") 701 | jupyter_transaction = {} 702 | else: 703 | pass 704 | 705 | except Exception as e: 706 | print("Jupyter Error", e) 707 | 708 | 709 | 710 | 711 | #Processing TokenAccounts Transactions 712 | async def process_token_account(self, token_account: Pubkey,wallet_address_id: int): 713 | transaction_data_dict = {} 714 | error_count = 0 715 | self.reset_variables() 716 | # Convert the Pubkey to a string for database operations 717 | token_account_str = str(token_account) 718 | 719 | # Directly process the transactions for the given token account 720 | sig = await self.async_solana_client.get_signatures_for_address(token_account, limit=500) 721 | valid=0 722 | # information_array = [] 723 | for signature in reversed(sig.value): 724 | if signature.err == None: 725 | transaction = await self.async_solana_client.get_transaction(signature.signature, encoding="jsonParsed", 726 | max_supported_transaction_version=0) 727 | txn_fee = transaction.value.transaction.meta.fee 728 | instruction_list = transaction.value.transaction.meta.inner_instructions 729 | account_signer = transaction.value.transaction.transaction.message.account_keys[0].pubkey 730 | decimals = transaction.value.transaction.meta.post_token_balances 731 | information_array = [] 732 | 733 | print(style.RED,signature.signature,style.RESET) 734 | for ui_inner_instructions in instruction_list: 735 | for txn_instructions in ui_inner_instructions.instructions: 736 | if txn_instructions.program_id == TOKEN_PROGRAM_ID: 737 | txn_information = txn_instructions.parsed['info'] 738 | if 'destination' in txn_information: 739 | information_array.append(txn_information) 740 | 741 | 742 | if account_signer == self.wallet_address: 743 | 744 | valid+=1 745 | 746 | 747 | 748 | 749 | 750 | 751 | try: 752 | if valid>0: 753 | await self.transactionDetails(transaction,information_array) 754 | 755 | print(style.CYAN,"Transaction Details Added",style.RESET) 756 | valid=0 757 | else: 758 | pass 759 | except Exception as e: 760 | print(e, signature.signature) 761 | print(style.RED,"Error Adding Transaction Details",style.RESET) 762 | continue 763 | 764 | await self.calculate_deltas() 765 | self.print_summary() 766 | 767 | await self.fill_pnl_info_table(token_account_str,wallet_address_id) 768 | 769 | async def pair_createdTime(self,token_traded): 770 | url = f'https://api.dexscreener.com/latest/dex/tokens/{token_traded}' 771 | 772 | response = requests.get(url) 773 | data = response.json() 774 | if data['pairs'] is not None: 775 | return round(data['pairs'][0]['pairCreatedAt']/1000) 776 | return 0 777 | 778 | 779 | def calculate_time_difference(self,unix_timestamp1, unix_timestamp2): 780 | """ 781 | Calculate the difference between two Unix timestamps and return it in a human-readable format. 782 | """ 783 | # Convert Unix timestamps to datetime objects 784 | date1 = self.convert_unix_to_date(round(unix_timestamp1)) 785 | date2 = self.convert_unix_to_date(round(unix_timestamp2)) 786 | 787 | # Calculate the difference between the two dates 788 | difference = date2 - date1 789 | 790 | # Calculate the difference in hours, minutes, and seconds 791 | hours, remainder = divmod(difference.total_seconds(), 3600) 792 | minutes, seconds = divmod(remainder, 60) 793 | 794 | # Format the output based on the difference 795 | if hours > 0: 796 | return f"{int(hours)}h {int(minutes)}m {int(seconds)}s" 797 | elif minutes > 0: 798 | return f"{int(minutes)}m {int(seconds)}s" 799 | else: 800 | return f"{int(seconds)}s" 801 | def update_buy(self, amount, fee, block_time): 802 | 803 | self.income += amount 804 | self.fee += fee 805 | self.buys += 1 806 | if self.first_buy_time is None: 807 | self.first_buy_time = block_time 808 | 809 | def update_sell(self, amount, fee, block_time): 810 | self.outcome += amount 811 | self.fee += fee 812 | self.sells += 1 813 | # Update the last sell time 814 | self.last_sell_time = block_time 815 | # Update the last trade to the highest block time 816 | if self.last_trade is None or block_time > self.last_trade: 817 | self.last_trade = block_time 818 | def print_summary(self): 819 | print(f"Income: {self.income}") 820 | print(f"Outcome: {self.outcome}") 821 | print(f"Total Fee: {self.fee/10**9}") 822 | print(f"Spent SOL: {self.spent_sol}") 823 | print(f"Earned SOL: {self.earned_sol}") 824 | print(f"Delta Token: {self.delta_token}") 825 | print(f"Delta SOL: {self.delta_sol}") 826 | print(f"Delta Percentage: {self.delta_percentage}%") 827 | print(f"Buys: {self.buys}") 828 | print(f"Sells: {self.sells}") 829 | print(f"Time Period: {self.time_period}") 830 | print(f"Contract: {self.contract}") 831 | print(f"Scam Tokens: {self.scam_tokens}") 832 | 833 | if self.tokenCreationTime == 0: 834 | buy_period = "Unknown" 835 | print("Buy Period:",buy_period) 836 | 837 | else: 838 | 839 | try: 840 | buy_period = self.calculate_time_difference(self.tokenCreationTime,self.first_buy_time ) 841 | print("Buy Period:",buy_period) 842 | except Exception as e: 843 | print(f"Error calculating buy period: {e}") 844 | buy_period = "No buy" 845 | # print(f"Buy Period :{self.calculate_time_difference(self.first_buy_time, self.tokenCreationTime) if self.tokenCreationTime != 0 or self.first_buy_time!= None else 0}") 846 | 847 | 848 | 849 | def reset_variables(self): 850 | self.income = 0 851 | self.outcome = 0 852 | self.fee = 0 853 | self.spent_sol = 0 854 | self.earned_sol = 0 855 | self.buys = 0 856 | self.sells = 0 857 | self.first_buy_time = None 858 | self.last_sell_time = None 859 | self.last_trade = None 860 | self.time_period = 0 861 | self.contract = None 862 | self.scam_tokens = 0 863 | self.tokenCreationTime = 0 864 | self.buy_period=0 865 | self.solAccount= None 866 | self.mint_decimal=None 867 | async def getToken_SolAmount(self,): 868 | token_address = str(self.contract) 869 | url = f'https://api.dexscreener.com/latest/dex/tokens/{token_address}' 870 | 871 | response = requests.get(url) 872 | data = response.json() 873 | if data['pairs'] is not None: 874 | token_price_usd = float(data['pairs'][0]['priceUsd']) 875 | wallet_amount= self.income 876 | Worth_wallet_amount = token_price_usd * wallet_amount 877 | worth_in_solana= Worth_wallet_amount / self.solana_price 878 | return worth_in_solana 879 | else: 880 | self.scam_tokens = 1 881 | return 0 882 | 883 | 884 | 885 | 886 | 887 | async def calculate_deltas(self): 888 | 889 | if self.sells >0: 890 | self.delta_token = self.income - self.outcome 891 | self.delta_sol = self.earned_sol - self.spent_sol 892 | self.delta_percentage = (self.delta_sol / self.spent_sol) * 100 if self.spent_sol != 0 else 0 893 | #self.delta_percentage = (self.delta_sol / self.spent_sol) * 100 if self.spent_sol != 0 else 0 894 | else: 895 | self.delta_token = self.income 896 | self.delta_sol = self.earned_sol - self.spent_sol 897 | self.delta_percentage = -100 898 | 899 | 900 | self.tokenCreationTime = await self.pair_createdTime(self.contract) 901 | if self.tokenCreationTime == 0: 902 | self.buy_period = "Unknown" 903 | 904 | else: 905 | self.buy_period = self.calculate_time_difference(self.tokenCreationTime, self.first_buy_time) 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | # Calculate the time period 917 | if self.first_buy_time and self.last_sell_time: 918 | time_difference = self.last_sell_time - self.first_buy_time 919 | self.time_period = self.calculate_time_difference(self.first_buy_time, self.last_sell_time) 920 | elif self.first_buy_time and not self.last_sell_time: 921 | self.time_period = 0 # No sell, indicating a potential scam 922 | self.scam_tokens = 1 923 | # self.last_sell_time = self.first_buy_time 924 | 925 | 926 | async def fill_pnl_info_table(self, token_account,wallet_address_id): 927 | """ 928 | Insert a new PNL info only if the transaction is not yet in the database. 929 | """ 930 | fields = [ 931 | 932 | ('token_account', token_account), 933 | ('income', self.income), 934 | ('outcome', self.outcome), 935 | ('fee', self.fee), 936 | ('spent_sol', self.spent_sol), 937 | ('earned_sol', self.earned_sol), 938 | ('delta_token', self.delta_token), 939 | ('delta_sol', self.delta_sol), 940 | ('buys', self.buys), 941 | ('sells', self.sells), 942 | ('time_period', self.time_period), 943 | ('contract', self.contract), 944 | ('scam_tokens', self.scam_tokens), 945 | ('wallet_address_id', wallet_address_id), 946 | ('last_trade', self.last_trade), 947 | ('buy_period',self.buy_period) 948 | ] 949 | 950 | none_fields = [field_name for field_name, field_value in fields if field_value is None] 951 | if none_fields: 952 | print(f"One or more fields are None: {', '.join(none_fields)}. Skipping the operation.") 953 | return 954 | # Prepare the SQL INSERT statement 955 | insert_sql = ''' 956 | INSERT INTO pnl_info ( 957 | wallet_address_id, token_account, income, outcome, total_fee, spent_sol, earned_sol, 958 | delta_token, delta_sol, delta_percentage, buys, sells, last_trade, 959 | time_period, contract, scam_tokens,buy_period 960 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?) 961 | ''' 962 | 963 | # Check if any field is None and skip the operation if so 964 | if any(value is None for value in 965 | [token_account, self.income, self.outcome, self.fee, self.spent_sol, self.earned_sol, self.delta_token, 966 | self.delta_sol, self.buys, self.sells, self.time_period, self.contract, self.scam_tokens, 967 | wallet_address_id, self.last_trade, self.calculate_time_difference(self.first_buy_time, self.tokenCreationTime) if self.tokenCreationTime != 0 or self.first_buy_time!= None else 0]): 968 | print("One or more fields are None. Skipping the operation.") 969 | return 970 | 971 | # Check if the transaction already exists in the database 972 | check_sql = ''' 973 | SELECT 1 FROM pnl_info 974 | WHERE wallet_address_id = ? AND last_trade = ? 975 | ''' 976 | cursor = self.conn.cursor() 977 | cursor.execute(check_sql, (wallet_address_id, self.last_trade)) 978 | if cursor.fetchone() is not None: 979 | print("Transaction already exists in the database. Skipping the operation.") 980 | return 981 | 982 | # Prepare the data to be inserted 983 | insert_data = ( 984 | wallet_address_id, 985 | token_account, # Use the token_account parameter 986 | self.income, 987 | self.outcome, 988 | self.fee / 10 ** 9, # Assuming the fee is in nanoseconds 989 | self.spent_sol, 990 | self.earned_sol, 991 | self.delta_token, 992 | self.delta_sol, 993 | self.delta_percentage, 994 | self.buys, 995 | self.sells, 996 | self.last_trade, 997 | self.time_period, 998 | str(self.contract), # Ensure this is a string 999 | self.scam_tokens, 1000 | self.buy_period 1001 | ) 1002 | 1003 | # Insert the new row 1004 | cursor.execute(insert_sql, insert_data) 1005 | self.conn.commit() 1006 | 1007 | print("PNL info successfully inserted into the database.") 1008 | 1009 | async def process_transactions(self): 1010 | 1011 | # print(f"Processing Address {self.wallet_address}") 1012 | 1013 | try: 1014 | self.wallet_address_id = self.get_wallet_address_id(self.wallet_address) 1015 | num_tokenAccounts = await self.get_token_accountsCount(self.wallet_address) 1016 | print(f"Processing Address {self.wallet_address} Number of Token Accounts {num_tokenAccounts}") 1017 | 1018 | 1019 | if num_tokenAccounts in range(1, int(config['max_token_accounts']) + 1): 1020 | print(f"Processing Address {self.wallet_address}") 1021 | await self.get_new_token_accounts(self.wallet_address) 1022 | 1023 | else: 1024 | print( 1025 | f"{style.RED}Skipping Wallet Address {self.wallet_address} with {num_tokenAccounts} token accounts", 1026 | style.RESET) 1027 | 1028 | 1029 | 1030 | except Exception as e: 1031 | print(e) 1032 | # print(f"Failed to process transaction {self.wallet_address} {e}") 1033 | 1034 | 1035 | async def run(): 1036 | processor = TransactionProcessor(Pubkey.from_string(config['wallet_address'])) 1037 | await processor.initialize() 1038 | if await processor.process_transactions(): 1039 | print("Eligible for report generation, proceeding.") 1040 | await processor.generate_reports_for_time_periods([90, 60, 30, 14, 7]) 1041 | print("Reports generated for all specified periods.") 1042 | else: 1043 | print("Did not meet criteria for report generation, skipping.") 1044 | 1045 | 1046 | asyncio.run(run()) 1047 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana-Wallet-Address-PNL-Bot 2 | Get a detailed Excel file of your Profit and Loss Ratio for your Solana Wallet Address. Check out [DEXPNL](https://github.com/henrytirla/DEX-PNL-BOT) repo for optimized version using DuneSQL API 3 | 4 | 5 | ## Generated reports 6 | https://drive.google.com/drive/folders/15INgNO8p9A30Ma5gaKzVlz8ZytV8Jmom 7 | 8 | ## How it works -Video 9 | [Solana Wallet Address PNL Bot](https://www.youtube.com/watch?v=C4f4RA-mLbc&t=166s&ab_channel=HenryTirla) 10 | 11 | ## 💨 Optmized version 12 | Utilizing Dune SQL the optimized version of this repo is found here : [DexPNL Bot](https://github.com/henrytirla/DEX-PNL-BOT). 13 | 14 | 15 | ## 💰 Support My Work 16 | If these scripts have helped you, please consider supporting my work. Your support will help me continue to develop these tools and create more useful resources for the crypto community. 17 | 18 | - 🤑 Fiat Donations: [Paypal Link](https://paypal.me/HenryTirla) 19 | 20 | - 🚀 henrytirla.sol: FJRDY392XSyfV9nFZC8SZij1hB3hsH121pCQi1KrvH6b 21 | 22 | -------------------------------------------------------------------------------- /Updated_Manual.py: -------------------------------------------------------------------------------- 1 | 2 | # Note a concurrent approach can be used if API does not limit you. Eg Deocode token accounts at the same time in batches of 10-20 3 | 4 | import os 5 | import sys 6 | import time 7 | 8 | import pandas as pd 9 | import layouts 10 | import asyncio 11 | import datetime 12 | import base58 13 | import websockets 14 | import json 15 | from solders.signature import Signature 16 | import requests 17 | from solders.pubkey import Pubkey 18 | from solana.rpc.api import Client, Keypair 19 | from solana.rpc.async_api import AsyncClient 20 | from datetime import datetime 21 | from spl.token.constants import TOKEN_PROGRAM_ID,WRAPPED_SOL_MINT 22 | 23 | from solana.rpc import types 24 | from solana.rpc.types import TokenAccountOpts 25 | from openpyxl import Workbook 26 | from openpyxl.styles import Font 27 | 28 | 29 | 30 | # from openpyxl.utils.dataframe import dataframe_to_rows 31 | from openpyxl.styles import NamedStyle, Font, Alignment, PatternFill, Border, Side 32 | from openpyxl.worksheet.datavalidation import DataValidation 33 | from openpyxl.formula.translate import Translator 34 | 35 | 36 | from openpyxl.utils import get_column_letter 37 | import re 38 | # LAMPORTS = 1000000000 39 | 40 | 41 | 42 | class style(): 43 | BLACK = '\033[30m' 44 | RED = '\033[31m' 45 | GREEN = '\033[32m' 46 | YELLOW = '\033[33m' 47 | BLUE = '\033[34m' 48 | MAGENTA = '\033[35m' 49 | CYAN = '\033[36m' 50 | WHITE = '\033[37m' 51 | UNDERLINE = '\033[4m' 52 | RESET = '\033[0m' 53 | # wallet_address = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" # 54 | seen_signatures = set() 55 | WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112" 56 | Pool_raydium="675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" 57 | raydium_V4="5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1" 58 | 59 | import sqlite3 60 | class TransactionProcessor: 61 | def __init__(self,wallet_address): 62 | self.queue = asyncio.Queue() 63 | self.wallet_address = wallet_address 64 | self.processing_token_accounts = 0 65 | # self.wallet_address_id_cache = {} # Initialize the cache 66 | 67 | 68 | # self.seen_signatures = set() 69 | self.async_solana_client = AsyncClient("RPC_HTTPS_URL") 70 | 71 | self.db_name = "manual.db" 72 | 73 | self.conn = sqlite3.connect(self.db_name) 74 | 75 | self.c = self.conn.cursor() 76 | self.init_db() 77 | self.wallet_address_id=self.get_wallet_address_id(wallet_address) 78 | 79 | self.income = 0 80 | self.outcome = 0 81 | self.fee = 0 82 | self.spent_sol = 0 83 | self.earned_sol = 0 84 | self.buys = 0 85 | self.sells = 0 86 | self.delta_sol=0 87 | self.delta_token=0 88 | self.delta_percentage=0 89 | self.first_buy_time = None 90 | self.last_sell_time = None 91 | self.last_trade = None 92 | self.time_period = 0 93 | 94 | self.contract = None 95 | self.scam_tokens = 0 96 | 97 | # self.sol_balance = self.getSOlBalance() 98 | self.sol_balance = None 99 | self.solana_price = self.get_current_solana_price() 100 | self.tokenCreationTime = None 101 | self.buy_period = 0 102 | 103 | 104 | def init_db(self): 105 | self.c.execute(''' 106 | CREATE TABLE IF NOT EXISTS wallet_address ( 107 | id INTEGER PRIMARY KEY AUTOINCREMENT, 108 | wallet_address TEXT UNIQUE 109 | ) 110 | ''') 111 | 112 | 113 | self.c.execute(''' 114 | CREATE TABLE IF NOT EXISTS token_accounts ( 115 | wallet_address_id INTEGER, 116 | wallet_token_account TEXT, 117 | block_time INTEGER, 118 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 119 | ) 120 | ''') 121 | 122 | self.c.execute(''' 123 | CREATE TABLE IF NOT EXISTS pnl_info ( 124 | token_account TEXT PRIMARY KEY, 125 | wallet_address_id INTEGER, 126 | income REAL, 127 | outcome REAL, 128 | total_fee REAL, 129 | spent_sol REAL, 130 | earned_sol REAL, 131 | delta_token REAL, 132 | delta_sol REAL, 133 | delta_percentage REAL, 134 | buys INTEGER, 135 | sells INTEGER, 136 | last_trade TEXT, 137 | time_period TEXT, 138 | contract TEXT, 139 | scam_tokens TEXT, 140 | buy_period TEXT, 141 | 142 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 143 | ) 144 | ''') 145 | 146 | 147 | self.c.execute(''' 148 | CREATE TABLE IF NOT EXISTS winning_wallets ( 149 | wallet_address_id INTEGER, 150 | win_rate_7 REAL, 151 | balance_change_7 REAL, 152 | token_accounts_7 INTEGER, 153 | win_rate_14 REAL, 154 | balance_change_14 REAL, 155 | token_accounts_14 INTEGER, 156 | win_rate_30 REAL, 157 | balance_change_30 REAL, 158 | token_accounts_30 INTEGER, 159 | win_rate_60 REAL, 160 | balance_change_60 REAL, 161 | token_accounts_60 INTEGER, 162 | win_rate_90 REAL, 163 | balance_change_90 REAL, 164 | token_accounts_90 INTEGER, 165 | FOREIGN KEY(wallet_address_id) REFERENCES wallet_address(id) 166 | ) 167 | ''') 168 | self.conn.commit() 169 | 170 | async def get_token_accountsCount(self,wallet_address: Pubkey): 171 | owner = wallet_address 172 | opts = types.TokenAccountOpts(program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) 173 | response = await self.async_solana_client.get_token_accounts_by_owner(owner, opts) 174 | # length= len(response.value) 175 | return len(response.value) 176 | 177 | 178 | async def initialize(self): 179 | self.sol_balance = await self.getSOlBalance() 180 | # self.tokenCreationTime = await self.pair_createdTime(self.wallet_address) 181 | 182 | 183 | ##REPORTING### 184 | async def getSOlBalance(self): 185 | pubkey = self.wallet_address 186 | response = await self.async_solana_client.get_balance(pubkey) 187 | balance = response.value / 10**9 188 | return balance 189 | 190 | def get_current_solana_price(self): 191 | url = "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd" 192 | response = requests.get(url) 193 | data = response.json() 194 | return data['solana']['usd'] if response.status_code == 200 else None 195 | 196 | 197 | def store_win_rate(self, time_period, win_rate, balance_change, token_accounts): 198 | # First, check if a row with the given wallet_address_id exists 199 | check_sql = 'SELECT 1 FROM winning_wallets WHERE wallet_address_id = ?' 200 | self.c.execute(check_sql, (self.wallet_address_id,)) 201 | row_exists = self.c.fetchone() is not None 202 | 203 | time_period_suffix = f"_{time_period}" 204 | 205 | if row_exists: 206 | # If the row exists, update the win_rate, balance_change, and token_accounts for the specified time_period 207 | update_sql = f''' 208 | UPDATE winning_wallets 209 | SET win_rate{time_period_suffix} = ?, 210 | balance_change{time_period_suffix} = ?, 211 | token_accounts{time_period_suffix} = ? 212 | WHERE wallet_address_id = ? 213 | ''' 214 | self.c.execute(update_sql, (win_rate, balance_change, token_accounts, self.wallet_address_id)) 215 | else: 216 | # If the row does not exist, insert a new row with default values set to NULL or 0 and then update 217 | # the specific time period data 218 | insert_sql = f''' 219 | INSERT INTO winning_wallets ( 220 | wallet_address_id, 221 | win_rate_7, balance_change_7, token_accounts_7, 222 | win_rate_14, balance_change_14, token_accounts_14, 223 | win_rate_30, balance_change_30, token_accounts_30, 224 | win_rate_60, balance_change_60, token_accounts_60, 225 | win_rate_90, balance_change_90, token_accounts_90 226 | ) VALUES ( 227 | ?, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL 228 | ) 229 | ''' 230 | self.c.execute(insert_sql, (self.wallet_address_id,)) 231 | # Now the row exists, update the specific time period data 232 | update_sql = f''' 233 | UPDATE winning_wallets 234 | SET win_rate{time_period_suffix} = ?, 235 | balance_change{time_period_suffix} = ?, 236 | token_accounts{time_period_suffix} = ? 237 | WHERE wallet_address_id = ? 238 | ''' 239 | self.c.execute(update_sql, (win_rate, balance_change, token_accounts, self.wallet_address_id)) 240 | 241 | # Commit the changes 242 | self.conn.commit() 243 | 244 | def get_summary(self, time_period): 245 | query = f''' 246 | WITH Calculations AS ( 247 | SELECT 248 | (SUM(CASE WHEN delta_sol > 0 THEN 1 ELSE 0 END) * 1.0 / COUNT(*)) * 100 AS WinRate, 249 | SUM(delta_sol) AS PnL_R, 250 | SUM(CASE WHEN delta_sol < 0 THEN delta_sol ELSE 0 END) AS PnL_Loss, 251 | (SUM(earned_sol) / NULLIF(SUM(spent_sol), 0) - 1) * 100 AS Balance_Change, 252 | COUNT(CASE WHEN scam_tokens = 1 THEN 1 END) AS ScamTokens 253 | FROM pnl_info 254 | WHERE wallet_address_id = ? 255 | AND last_trade >= strftime('%s', 'now', '-{time_period} days') 256 | ) 257 | 258 | SELECT 259 | *, 260 | '{time_period} days' AS TimePeriod 261 | FROM Calculations; 262 | ''' 263 | self.c.execute(query, (self.get_wallet_address_id(self.wallet_address),)) 264 | 265 | summary_result = self.c.fetchone() 266 | 267 | summary_data = { 268 | 'SolBalance': self.sol_balance, 269 | 'WalletAddress': str(self.wallet_address), 270 | 'WinRate': summary_result[0], 271 | 'PnL_R': summary_result[1], 272 | 'PnL_Loss': summary_result[2], 273 | 'Balance_Change': summary_result[3], 274 | 'ScamTokens': summary_result[4], 275 | 'TimePeriod': summary_result[5] # Assuming ScamTokens is the 6th column in the result 276 | } 277 | 278 | return summary_data 279 | 280 | def get_transactions(self, time_period): 281 | query = f''' 282 | SELECT * 283 | FROM pnl_info 284 | WHERE wallet_address_id = ? 285 | AND last_trade >= strftime('%s', 'now', '-{time_period} days'); 286 | ''' 287 | self.c.execute(query, (self.wallet_address_id,)) 288 | results = self.c.fetchall() 289 | # print(results) 290 | #return results 291 | transactions_df = pd.DataFrame(results, columns=['token_account', 'wallet_address_id', 'income', 'outcome', 292 | 'total_fee', 'spent_sol', 'earned_sol', 'delta_token', 293 | 'delta_sol', 'delta_percentage', 'buys', 'sells', 294 | 'last_trade', 'time_period', 'contract', 'scam token','buy_period']) 295 | 296 | # Sort transactions by 'last_trade' in descending order 297 | transactions_df = transactions_df.sort_values(by='last_trade', ascending=False) 298 | # sum_delta_sol = transactions_df['delta_sol'].sum() 299 | # print(f"Sum of delta_sol: {sum_delta_sol}") 300 | 301 | return transactions_df 302 | 303 | async def generate_reports_for_time_periods(self, time_periods): 304 | await self.initialize() 305 | 306 | # Fetch the wallet address ID from the database 307 | self.wallet_address_id = self.wallet_address_id 308 | 309 | # Directory to store the reports 310 | reports_folder = "processing_reports" 311 | if not os.path.exists(reports_folder): 312 | os.makedirs(reports_folder) 313 | wallet_folder = os.path.join(reports_folder, str(self.wallet_address)) 314 | if not os.path.exists(wallet_folder): 315 | os.makedirs(wallet_folder) 316 | 317 | # Generate and export reports for each specified time period 318 | for time_period in time_periods: 319 | summary = self.get_summary(time_period) 320 | # print(time_period,summary) 321 | transactions = self.get_transactions(time_period) 322 | if summary: 323 | 324 | file_name = os.path.join(wallet_folder, f"{self.wallet_address}_{time_period}_days.xlsx") 325 | self.export_to_excel(summary, transactions, file_name) 326 | print(f"Exported summary and transactions for {time_period} days to {file_name}") 327 | else: 328 | print(f"No summary found for {time_period} days.") 329 | 330 | def export_to_excel(self,summary, transactions, file_name): 331 | sol_current_price = self.solana_price 332 | # print(transactions) 333 | summary['Current_Sol_Price'] = sol_current_price 334 | summary['ID'] = self.wallet_address_id 335 | win_rate =summary['WinRate'] 336 | 337 | try: 338 | summary['Profit_USD'] = summary['PnL_R'] * sol_current_price 339 | summary['Loss_USD'] = abs(summary['PnL_Loss']) * sol_current_price 340 | except TypeError: 341 | summary['Profit_USD'] = 0 342 | summary['Loss_USD'] = 0 343 | 344 | # Convert summary dictionary to DataFrame 345 | summary_df = pd.DataFrame([summary], columns=['ID','WalletAddress', 'SolBalance','Current_Sol_Price', 'WinRate', 'PnL_R', 'PnL_Loss', 346 | 'Balance_Change', 'ScamTokens', 'Profit_USD', 347 | 'Loss_USD', 'TimePeriod']) 348 | 349 | # Convert transactions to DataFrame 350 | transactions_df = pd.DataFrame(transactions, columns=['token_account','wallet_address_id', 'income', 'outcome', 351 | 'total_fee', 'spent_sol', 'earned_sol', 'delta_token', 352 | 'delta_sol', 'delta_percentage', 'buys', 'sells', 353 | 'last_trade', 'time_period','buy_period', 'contract', 'scam token']) 354 | transactions_df.drop(columns=['wallet_address_id'], inplace=True) 355 | transactions_df['last_trade'] = transactions_df['last_trade'].apply(lambda x: datetime.fromtimestamp(int(x)).strftime('%d.%m.%Y')) 356 | 357 | 358 | with pd.ExcelWriter(file_name, engine='openpyxl') as writer: 359 | 360 | # Write DataFrames to Excel 361 | summary_df.to_excel(writer, sheet_name='Summary and Transactions', index=False, startrow=0) 362 | row_to_start = len(summary_df) + 2 # Adjust row spacing 363 | transactions_df.to_excel(writer, sheet_name='Summary and Transactions', index=False, startrow=row_to_start) 364 | 365 | workbook = writer.book 366 | worksheet = writer.sheets['Summary and Transactions'] 367 | 368 | for row in worksheet.iter_rows(min_row=row_to_start + 2, max_col=worksheet.max_column): 369 | for cell in row: 370 | if cell.column == 1: # 'token_account' column 371 | cell.hyperlink = f'https://solscan.io/account/{cell.value}#splTransfer' 372 | cell.value = 'View Solscan' 373 | cell.font = Font(underline='single') 374 | elif cell.column == 15: # 'contract' column 375 | cell.hyperlink = f'https://dexscreener.com/solana/{cell.value}?maker={self.wallet_address}' 376 | 'https://dexscreener.com/solana/3zcoadmvqtx3itfthwr946nhznqh92eq9hdhhjtgp6as?maker=3uij3uDg5pLBBxQw6hUXxqw6uEwCQGX7rM8PZb7ofH9e' 377 | cell.value = 'View Dexscreener' 378 | cell.font = Font(underline='single') 379 | 380 | 381 | # Define fills for conditional formatting 382 | red_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") 383 | green_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid") 384 | yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") 385 | brown_fill = PatternFill(start_color="A52A2A", end_color="A52A2A", fill_type="solid") 386 | gold_fill = PatternFill(start_color="FFD700", end_color="FFD700", fill_type="solid") 387 | 388 | # Apply initial styling 389 | for row in worksheet.iter_rows(min_row=1, max_row=worksheet.max_row, min_col=1, 390 | max_col=worksheet.max_column): 391 | for cell in row: 392 | cell.border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), 393 | bottom=Side(style='thin')) 394 | cell.alignment = Alignment(horizontal="center", vertical="center") 395 | if cell.row == 1: 396 | cell.font = Font(bold=True) 397 | 398 | # Adjust column widths 399 | for column_cells in worksheet.columns: 400 | length = max(len(str(cell.value)) for cell in column_cells) 401 | worksheet.column_dimensions[get_column_letter(column_cells[0].column)].width = length + 2 402 | 403 | # Conditional formatting for transactions 404 | for idx, row in enumerate(transactions_df.itertuples(index=False), start=row_to_start + 2): 405 | # Assuming 'outcome' is the 4th column, 'delta_percentage' is the 10th, 'time_period' is the 15th, and 'buys' is the 11th 406 | outcome = row[2] 407 | income=row[1] 408 | # income=row[2] 409 | delta_percentage = row[8] 410 | # print(int(delta_percentage)) 411 | # sys.exit() 412 | time_period = row[12] 413 | buys = row[9] 414 | 415 | 416 | if round(outcome,1) > round(income,1): 417 | worksheet.cell(row=idx, column=3).fill = yellow_fill 418 | 419 | 420 | if delta_percentage ==-100: 421 | 422 | worksheet.cell(row=idx, column=9).fill = brown_fill 423 | 424 | if pd.to_timedelta(time_period) < pd.Timedelta(minutes=1): 425 | worksheet.cell(row=idx, column=13).fill = yellow_fill 426 | 427 | if buys > 3: 428 | worksheet.cell(row=idx, column=10).fill = yellow_fill 429 | 430 | # Additional conditional formatting for delta_sol and delta_percentage 431 | # Assuming 'delta_sol' is the 9th column and 'delta_percentage' is the 10th column 432 | for idx, row in enumerate(transactions_df.itertuples(index=False), start=row_to_start + 2): 433 | # For delta_sol 434 | if row[7] < 0: 435 | worksheet.cell(row=idx, column=8).fill = red_fill 436 | elif row[7] > 0: 437 | worksheet.cell(row=idx, column=8).fill = green_fill 438 | 439 | # For delta_percentage 440 | 441 | if row[7] < 0 and row[8] != -100: 442 | worksheet.cell(row=idx, column=9).fill = red_fill 443 | elif row[8] > 0: 444 | worksheet.cell(row=idx, column=9).fill = green_fill 445 | 446 | 447 | for idx, row in enumerate(summary_df.itertuples(index=False), start=1): 448 | # print(row[9], row[10]) 449 | if row[9] > row[10] and row[4]>=50: # If Profit_USD > Loss_USD 450 | worksheet.cell(row=idx, column=10).fill = gold_fill 451 | worksheet.cell(row=idx, column=5).fill = gold_fill 452 | 453 | elif row[9] Loss_USD 454 | worksheet.cell(row=idx, column=10).fill = red_fill 455 | worksheet.cell(row=idx, column=5).fill = red_fill 456 | 457 | elif row[9] < row[10] and row[4] == 50: # If Profit_USD > Loss_USD 458 | worksheet.cell(row=idx, column=10).fill = red_fill 459 | worksheet.cell(row=idx, column=5).fill = gold_fill 460 | 461 | elif row[9] > row[10] and row[4] < 50: 462 | worksheet.cell(row=idx, column=10).fill = gold_fill 463 | worksheet.cell(row=idx, column=5).fill = red_fill 464 | else: 465 | worksheet.cell(row=idx, column=11).fill = red_fill 466 | worksheet.cell(row=idx, column=5).fill = red_fill 467 | 468 | 469 | 470 | 471 | # Save the workbook 472 | workbook.save(file_name) 473 | 474 | 475 | 476 | 477 | def get_wallet_address_id(self,wallet_address): 478 | # Check if the wallet address exists in the database 479 | self.c.execute('SELECT id FROM wallet_address WHERE wallet_address = ?', (str(wallet_address),)) 480 | result = self.c.fetchone() 481 | 482 | if result: 483 | # If the wallet address exists, return the ID 484 | return result[0] 485 | else: 486 | # If the wallet address does not exist, insert it into the database 487 | self.c.execute('INSERT INTO wallet_address (wallet_address) VALUES (?)', (str(wallet_address),)) 488 | # Commit the transaction to save the changes to the database 489 | self.conn.commit() 490 | # Return the ID of the newly inserted record 491 | return self.c.lastrowid 492 | 493 | def convert_unix_to_date(self,unix_timestamp): 494 | """ 495 | Convert a Unix timestamp to a datetime object. 496 | 497 | """ 498 | timestamp_str = str(unix_timestamp) 499 | # If the length is greater than 10 digits, it's likely in milliseconds 500 | if len(timestamp_str) > 10: 501 | print("retruned",datetime.fromtimestamp(round(unix_timestamp / 1000))) 502 | return datetime.fromtimestamp(round(unix_timestamp / 1000)) 503 | 504 | return datetime.fromtimestamp(unix_timestamp) 505 | 506 | async def update_token_account(self, wallet_address, wallet_token_account, block_time, wallet_address_id): 507 | try: 508 | # Convert the Pubkey to a string 509 | print("Update Wallet ID",wallet_address_id) 510 | 511 | wallet_token_account_str = str(wallet_token_account) 512 | 513 | # Insert a new token account without checking if it exists 514 | self.c.execute( 515 | 'INSERT INTO token_accounts (wallet_address_id, wallet_token_account, block_time) VALUES (?, ?, ?)', 516 | (wallet_address_id, wallet_token_account_str, block_time)) 517 | # Commit the changes 518 | self.conn.commit() 519 | print( 520 | f"{style.CYAN}New token account added for wallet address: {str(wallet_address)}, token account: {wallet_token_account_str}", 521 | style.RESET) 522 | # print("Calculating and updating pnl") 523 | # print(wallet_token_account) 524 | # await self.process_token_account(wallet_token_account, wallet_address_id) 525 | 526 | except Exception as e: 527 | print(f"Error updating token account: {e}") 528 | # Rollback the transaction in case of an error 529 | self.conn.rollback() 530 | 531 | 532 | def get_token_data(self,decimals): 533 | for token_balances in decimals: 534 | if token_balances.owner == self.wallet_address and token_balances.mint != Pubkey.from_string(WRAPPED_SOL_MINT): 535 | token_contract = token_balances.mint 536 | token_decimal = token_balances.ui_token_amount.decimals 537 | if token_contract is None: 538 | token_contract = "Unknown" 539 | 540 | 541 | return token_contract, token_decimal 542 | async def transactionType(self,Account:str): 543 | data_response = await self.async_solana_client.get_account_info(Pubkey.from_string(Account)) 544 | data = data_response.value.data 545 | parsed_data = layouts.SPL_ACCOUNT_LAYOUT.parse(data) 546 | # print("Parsed Data",parsed_data) 547 | mint = Pubkey.from_bytes(parsed_data.mint) 548 | if mint == WRAPPED_SOL_MINT: 549 | return mint 550 | return mint 551 | 552 | 553 | 554 | async def transactionDetails(self, block_time, txn_fee, information_array: list, token_traded: str, 555 | token_decimal: int): 556 | 557 | try: 558 | first_info = information_array[0] 559 | second_info = information_array[1] 560 | first_authority = first_info['authority'] 561 | t_type= await self.transactionType(second_info['source']) 562 | # print(first_info) 563 | # print("----") 564 | # print(second_info) 565 | 566 | if first_authority == str(self.wallet_address) and str(t_type) != WRAPPED_SOL_MINT : 567 | first_amount = int(first_info['amount']) / 10 ** 9 568 | self.update_buy(int(second_info['amount']) / 10 ** token_decimal, txn_fee, block_time) 569 | self.spent_sol += first_amount 570 | self.contract = token_traded 571 | self.last_trade = block_time 572 | print(f"{style.GREEN}BUY {first_amount} SOL {style.RESET} -FOR {int(second_info['amount']) / 10 ** token_decimal} TokenBought= {self.contract} ") 573 | 574 | else: 575 | first_amount = int(first_info['amount']) / 10 ** token_decimal 576 | self.update_sell(first_amount, txn_fee, block_time) 577 | self.earned_sol += int(second_info['amount']) / 10 ** 9 578 | self.contract = token_traded 579 | self.last_trade = block_time 580 | print( 581 | f"{style.RED}SELL{style.RESET} {first_amount} -{self.contract}--FOR {style.GREEN}{int(second_info['amount']) / 10 ** 9} {style.RESET}SOL") 582 | except Exception as e: 583 | print(f"Error processing transaction details: {e}") 584 | 585 | 586 | 587 | 588 | #Processing TokenAccounts Transactions 589 | async def process_token_account(self, token_accounts: list,wallet_address_id: int): 590 | # transaction_data_dict = {} 591 | # error_count = 0 592 | self.processing_token_accounts= len(token_accounts) 593 | 594 | while self.processing_token_accounts>0: 595 | 596 | try: 597 | # Convert the Pubkey to a string for database operations 598 | for token_account in token_accounts: 599 | self.reset_variables() 600 | print(token_account,"Number of token Account to be processed",len(token_accounts)) 601 | 602 | token_account_str = str(token_account) 603 | # Directly process the transactions for the given token account 604 | # await asyncio.sleep(1) 605 | sig = await self.async_solana_client.get_signatures_for_address(Pubkey.from_string(token_account), limit=500) 606 | # information_array = [] 607 | last_signature = await self.async_solana_client.get_transaction(sig.value[-1].signature, 608 | encoding="jsonParsed", 609 | max_supported_transaction_version=0) 610 | # print(last_signature) 611 | 612 | block_time = last_signature.value.block_time 613 | try: 614 | 615 | await self.update_token_account(self.wallet_address, Pubkey.from_string(token_account_str), block_time,wallet_address_id) 616 | 617 | except Exception as e: 618 | print(f"The problem is here: {e}") 619 | 620 | print("UPDATING TOKEN ACCOUNT DONE") 621 | for signature in reversed(sig.value): 622 | if signature.err == None: 623 | transaction = await self.async_solana_client.get_transaction(signature.signature, encoding="jsonParsed", 624 | max_supported_transaction_version=0) 625 | 626 | txn_fee = transaction.value.transaction.meta.fee 627 | instruction_list = transaction.value.transaction.meta.inner_instructions 628 | account_signer = transaction.value.transaction.transaction.message.account_keys[0].pubkey 629 | decimals = transaction.value.transaction.meta.post_token_balances 630 | information_array = [] 631 | 632 | # print(decimals) 633 | print(style.RED,signature.signature,style.RESET) 634 | print(account_signer,self.wallet_address) 635 | 636 | if account_signer == self.wallet_address: 637 | for ui_inner_instructions in instruction_list: 638 | for txn_instructions in ui_inner_instructions.instructions: 639 | if txn_instructions.program_id == TOKEN_PROGRAM_ID: 640 | txn_information = txn_instructions.parsed['info'] 641 | if 'destination' in txn_information: 642 | information_array.append(txn_information) 643 | 644 | try: 645 | # print(block_time) 646 | block_time = transaction.value.block_time 647 | 648 | token_traded, token_decimal = self.get_token_data(decimals) 649 | # print(token_traded,token_decimal) 650 | await self.transactionDetails(block_time, txn_fee, information_array, token_traded, 651 | token_decimal) 652 | except Exception as e: 653 | print(e, signature.signature) 654 | print(style.RED, "Error Adding Transaction Details", style.RESET) 655 | continue 656 | 657 | print("Calculating and updating pnl") 658 | # self.print_summary() 659 | 660 | 661 | await self.calculate_deltas() 662 | self.print_summary() 663 | 664 | await self.fill_pnl_info_table(token_account_str,wallet_address_id) 665 | token_accounts.remove(token_account) 666 | self.processing_token_accounts =len(token_accounts) 667 | 668 | 669 | except Exception as e: 670 | print(f"Error processing token account: {e},{token_account}") 671 | # token_accounts.remove(token_account) 672 | 673 | if str(e) =="type NoneType doesn't define __round__ method": 674 | token_accounts.remove(token_account) 675 | 676 | # Rollback the transaction in case of an error 677 | continue 678 | 679 | async def pair_createdTime(self,token_traded): 680 | url = f'https://api.dexscreener.com/latest/dex/tokens/{token_traded}' 681 | 682 | response = requests.get(url) 683 | data = response.json() 684 | if data['pairs'] is not None: 685 | return round(data['pairs'][0]['pairCreatedAt']/1000) 686 | return 0 687 | 688 | 689 | def calculate_time_difference(self,unix_timestamp1, unix_timestamp2): 690 | """ 691 | Calculate the difference between two Unix timestamps and return it in a human-readable format. 692 | """ 693 | # Convert Unix timestamps to datetime objects 694 | date1 = self.convert_unix_to_date(round(unix_timestamp1)) 695 | date2 = self.convert_unix_to_date(round(unix_timestamp2)) 696 | 697 | # Calculate the difference between the two dates 698 | difference = date2 - date1 699 | 700 | # Calculate the difference in hours, minutes, and seconds 701 | hours, remainder = divmod(difference.total_seconds(), 3600) 702 | minutes, seconds = divmod(remainder, 60) 703 | 704 | # Format the output based on the difference 705 | if hours > 0: 706 | return f"{int(hours)}h {int(minutes)}m {int(seconds)}s" 707 | elif minutes > 0: 708 | return f"{int(minutes)}m {int(seconds)}s" 709 | else: 710 | return f"{int(seconds)}s" 711 | def update_buy(self, amount, fee, block_time): 712 | 713 | self.income += amount 714 | self.fee += fee 715 | self.buys += 1 716 | if self.first_buy_time is None: 717 | self.first_buy_time = block_time 718 | 719 | def update_sell(self, amount, fee, block_time): 720 | self.outcome += amount 721 | self.fee += fee 722 | self.sells += 1 723 | # Update the last sell time 724 | self.last_sell_time = block_time 725 | # Update the last trade to the highest block time 726 | if self.last_trade is None or block_time > self.last_trade: 727 | self.last_trade = block_time 728 | def print_summary(self): 729 | print(f"Income: {self.income}") 730 | print(f"Outcome: {self.outcome}") 731 | print(f"Total Fee: {self.fee/10**9}") 732 | print(f"Spent SOL: {self.spent_sol}") 733 | print(f"Earned SOL: {self.earned_sol}") 734 | print(f"Delta Token: {self.delta_token}") 735 | print(f"Delta SOL: {self.delta_sol}") 736 | print(f"Delta Percentage: {self.delta_percentage}%") 737 | print(f"Buys: {self.buys}") 738 | print(f"Sells: {self.sells}") 739 | 740 | print(f"Time Period: {self.time_period}") 741 | print(f"Contract: {self.contract}") 742 | print(f"Scam Tokens: {self.scam_tokens}") 743 | 744 | 745 | if self.tokenCreationTime == 0: 746 | buy_period = "Unknown" 747 | print("Buy Period:",buy_period) 748 | print(self.contract) 749 | 750 | else: 751 | 752 | try: 753 | buy_period = self.calculate_time_difference(self.tokenCreationTime,self.first_buy_time ) 754 | print("Buy Period:",buy_period) 755 | except Exception as e: 756 | print(f"Error calculating buy period: {e}") 757 | buy_period = "No buy" 758 | 759 | 760 | 761 | def reset_variables(self): 762 | self.income = 0 763 | self.outcome = 0 764 | self.fee = 0 765 | self.spent_sol = 0 766 | self.earned_sol = 0 767 | self.buys = 0 768 | self.sells = 0 769 | self.first_buy_time = None 770 | self.last_sell_time = None 771 | self.last_trade = None 772 | self.time_period = 0 773 | self.contract = None 774 | self.scam_tokens = 0 775 | self.tokenCreationTime = 0 776 | self.buy_period=0 777 | async def getToken_SolAmount(self,): 778 | token_address = str(self.contract) 779 | url = f'https://api.dexscreener.com/latest/dex/tokens/{token_address}' 780 | 781 | response = requests.get(url) 782 | data = response.json() 783 | if data['pairs'] is not None: 784 | token_price_usd = float(data['pairs'][0]['priceUsd']) 785 | wallet_amount= self.income 786 | Worth_wallet_amount = token_price_usd * wallet_amount 787 | worth_in_solana= Worth_wallet_amount / self.solana_price 788 | return worth_in_solana 789 | else: 790 | self.scam_tokens = 1 791 | return 0 792 | 793 | 794 | 795 | 796 | 797 | async def calculate_deltas(self): 798 | 799 | if self.sells >0: 800 | self.delta_token = self.income - self.outcome 801 | self.delta_sol = self.earned_sol - self.spent_sol 802 | self.delta_percentage = (self.delta_sol / self.spent_sol) * 100 if self.spent_sol != 0 else 0 803 | else: 804 | self.delta_token = self.income 805 | self.delta_sol = self.earned_sol - self.spent_sol 806 | self.delta_percentage = -100 807 | 808 | 809 | self.tokenCreationTime = await self.pair_createdTime(self.contract) 810 | if self.tokenCreationTime == 0: 811 | self.buy_period = "Unknown" 812 | 813 | else: 814 | self.buy_period = self.calculate_time_difference(self.tokenCreationTime, self.first_buy_time) 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | if self.first_buy_time and self.last_sell_time: 827 | time_difference = self.last_sell_time - self.first_buy_time 828 | self.time_period = self.calculate_time_difference(self.first_buy_time, self.last_sell_time) 829 | elif self.first_buy_time and not self.last_sell_time: 830 | self.time_period = 0 # No sell, indicating a potential scam 831 | self.scam_tokens = 1 832 | # self.last_sell_time = self.first_buy_time 833 | 834 | 835 | async def fill_pnl_info_table(self, token_account,wallet_address_id): 836 | """ 837 | Insert a new PNL info only if the transaction is not yet in the database. 838 | """ 839 | try: 840 | fields = [ 841 | 842 | ('token_account', token_account), 843 | ('income', self.income), 844 | ('outcome', self.outcome), 845 | ('fee', self.fee), 846 | ('spent_sol', self.spent_sol), 847 | ('earned_sol', self.earned_sol), 848 | ('delta_token', self.delta_token), 849 | ('delta_sol', self.delta_sol), 850 | ('buys', self.buys), 851 | ('sells', self.sells), 852 | ('time_period', self.time_period), 853 | ('contract', self.contract), 854 | ('scam_tokens', self.scam_tokens), 855 | ('wallet_address_id', wallet_address_id), 856 | ('last_trade', self.last_trade), 857 | ('buy_period',self.buy_period) 858 | ] 859 | 860 | none_fields = [field_name for field_name, field_value in fields if field_value is None] 861 | if none_fields: 862 | print(f"One or more fields are None: {', '.join(none_fields)}. Skipping the operation.") 863 | return 864 | # Prepare the SQL INSERT statement 865 | insert_sql = ''' 866 | INSERT INTO pnl_info ( 867 | wallet_address_id, token_account, income, outcome, total_fee, spent_sol, earned_sol, 868 | delta_token, delta_sol, delta_percentage, buys, sells, last_trade, 869 | time_period, contract, scam_tokens,buy_period 870 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?) 871 | ''' 872 | 873 | # Check if any field is None and skip the operation if so 874 | if any(value is None for value in 875 | [token_account, self.income, self.outcome, self.fee, self.spent_sol, self.earned_sol, self.delta_token, 876 | self.delta_sol, self.buys, self.sells, self.time_period, self.contract, self.scam_tokens, 877 | wallet_address_id, self.last_trade, self.calculate_time_difference(self.first_buy_time, self.tokenCreationTime) if self.tokenCreationTime != 0 or self.first_buy_time!= None else 0]): 878 | print("One or more fields are None. Skipping the operation.") 879 | return 880 | 881 | # Check if the transaction already exists in the database 882 | check_sql = ''' 883 | SELECT 1 FROM pnl_info 884 | WHERE wallet_address_id = ? AND last_trade = ? 885 | ''' 886 | cursor = self.conn.cursor() 887 | cursor.execute(check_sql, (wallet_address_id, self.last_trade)) 888 | if cursor.fetchone() is not None: 889 | print("Transaction already exists in the database. Skipping the operation.") 890 | return 891 | 892 | # Prepare the data to be inserted 893 | insert_data = ( 894 | wallet_address_id, 895 | token_account, # Use the token_account parameter 896 | self.income, 897 | self.outcome, 898 | self.fee / 10 ** 9, # Assuming the fee is in nanoseconds 899 | self.spent_sol, 900 | self.earned_sol, 901 | self.delta_token, 902 | self.delta_sol, 903 | self.delta_percentage, 904 | # (self.delta_sol / self.spent_sol) * 100 if self.spent_sol != 0 else 0, 905 | self.buys, 906 | self.sells, 907 | # self.convert_unix_to_date(self.last_trade), 908 | self.last_trade, 909 | self.time_period, 910 | str(self.contract), # Ensure this is a string 911 | self.scam_tokens, 912 | self.buy_period 913 | ) 914 | 915 | # Insert the new row 916 | cursor.execute(insert_sql, insert_data) 917 | self.conn.commit() 918 | 919 | print("PNL info successfully inserted into the database.") 920 | except Exception as e: 921 | print(f"Filling info issue, {e}") 922 | 923 | async def process_transactions(self): 924 | 925 | # print(f"Processing Address {self.wallet_address}") 926 | 927 | try: 928 | 929 | 930 | 931 | wallet_address_id = self.wallet_address_id 932 | 933 | 934 | owner = self.wallet_address 935 | opts = TokenAccountOpts( 936 | program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") 937 | ) 938 | response = await self.async_solana_client.get_token_accounts_by_owner(owner, opts) 939 | solana_token_accounts = {str(token_account.pubkey): token_account for token_account in response.value} 940 | 941 | num_tokenAccounts= len(solana_token_accounts) 942 | print("Number of token Accounts",num_tokenAccounts) 943 | # Fetch all token accounts from the database for the wallet address 944 | self.c.execute('SELECT wallet_token_account FROM token_accounts WHERE wallet_address_id = ?', 945 | (wallet_address_id,)) 946 | db_token_accounts = {row[0] for row in self.c.fetchall()} 947 | newTokenAccounts = solana_token_accounts.keys() - db_token_accounts 948 | new_token_accounts = list(newTokenAccounts) 949 | 950 | 951 | if not newTokenAccounts: 952 | print("No new token accounts") 953 | return 954 | 955 | # Process token accounts if they are within the desired range 956 | if 1 <= len(newTokenAccounts) < 15000: 957 | 958 | print( 959 | f"Processing Address {self.wallet_address} Number of Token Accounts to be Processed {len(newTokenAccounts)}") 960 | new_token_accounts = list(newTokenAccounts) 961 | await self.process_token_account(new_token_accounts, wallet_address_id) 962 | print("ALL TOKEN ACCOUNTS PROCESSED") 963 | else: 964 | print( 965 | f"{style.RED}Skipping Wallet Address {self.wallet_address} with {len(newTokenAccounts)} token accounts", 966 | style.RESET) 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | except Exception as e: 978 | print(e) 979 | # pass 980 | print(f"Failed to process transaction {self.wallet_address} {e}") 981 | 982 | 983 | 984 | async def run(): 985 | 986 | processor = TransactionProcessor(Pubkey.from_string("EWzk2847WCPis45hPQ6Em5UuRbZiP1CSmqx7nG9ELd2L")) 987 | 988 | # Initialize the processor, ensuring it's ready to process transactions 989 | await processor.initialize() 990 | 991 | # This will process all token accounts and return control when done 992 | await processor.process_transactions() 993 | 994 | # Now generate reports. This will only be called after all token accounts are processed 995 | await processor.generate_reports_for_time_periods([90, 60, 30, 14, 7,1]) 996 | 997 | print("All accounts processed and reports generated.") 998 | 999 | 1000 | 1001 | asyncio.run(run()) 1002 | -------------------------------------------------------------------------------- /installation.md: -------------------------------------------------------------------------------- 1 | ## How to install dependencies 2 | 3 | 1. Install the required packages by running the following command: 4 | ```pip install -r requirements.txt``` 5 | 2. Add the following to .env file: Your RPC node URL, Wallet address you want to get the PNL,Token Account Limits 6 | 3. -------------------------------------------------------------------------------- /layouts.py: -------------------------------------------------------------------------------- 1 | from borsh_construct import CStruct, U64 2 | from construct import Bytes, Int8ul, Int32ul, Int64ul, Pass, Switch 3 | 4 | PUBLIC_KEY_LAYOUT = Bytes(32) 5 | 6 | SPL_ACCOUNT_LAYOUT = CStruct( 7 | "mint" / PUBLIC_KEY_LAYOUT, 8 | "owner" / PUBLIC_KEY_LAYOUT, 9 | "amount" / U64, 10 | "delegateOption" / Int32ul, 11 | "delegate" / PUBLIC_KEY_LAYOUT, 12 | "state" / Int8ul, 13 | "isNativeOption" / Int32ul, 14 | "isNative" / U64, 15 | "delegatedAmount" / U64, 16 | "closeAuthorityOption" / Int32ul, 17 | "closeAuthority" / PUBLIC_KEY_LAYOUT 18 | ) 19 | SPL_MINT_LAYOUT = CStruct( 20 | "mintAuthorityOption"/ Int32ul, 21 | 'mintAuthority'/PUBLIC_KEY_LAYOUT, 22 | 'supply'/U64, 23 | 'decimals'/Int8ul, 24 | 'isInitialized'/Int8ul, 25 | 'freezeAuthorityOption'/Int32ul, 26 | 'freezeAuthority'/PUBLIC_KEY_LAYOUT 27 | ) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==4.3.0 2 | certifi==2024.2.2 3 | charset-normalizer==3.3.2 4 | construct==2.10.68 5 | construct-typing==0.5.6 6 | et-xmlfile==1.1.0 7 | h11==0.14.0 8 | httpcore==1.0.5 9 | httpx==0.27.0 10 | idna==3.7 11 | jsonalias==0.1.1 12 | openpyxl==3.1.2 13 | requests==2.31.0 14 | setuptools==69.5.1 15 | sniffio==1.3.1 16 | solana==0.33.0 17 | solders==0.21.0 18 | typing_extensions==4.11.0 19 | urllib3==2.2.1 20 | websockets==11.0.3 21 | --------------------------------------------------------------------------------