Files
pumpfun-bonkfun-bot_github/src/trading/position.py
T
2025-07-18 12:03:00 +00:00

167 lines
5.1 KiB
Python

"""
Position management for take profit/stop loss functionality.
"""
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from solders.pubkey import Pubkey
class ExitReason(Enum):
"""Reasons for position exit."""
TAKE_PROFIT = "take_profit"
STOP_LOSS = "stop_loss"
MAX_HOLD_TIME = "max_hold_time"
MANUAL = "manual"
@dataclass
class Position:
"""Represents an active trading position."""
# Token information
mint: Pubkey
symbol: str
# Position details
entry_price: float
quantity: float
entry_time: datetime
# Exit conditions
take_profit_price: float | None = None
stop_loss_price: float | None = None
max_hold_time: int | None = None # seconds
# Status
is_active: bool = True
exit_reason: ExitReason | None = None
exit_price: float | None = None
exit_time: datetime | None = None
@classmethod
def create_from_buy_result(
cls,
mint: Pubkey,
symbol: str,
entry_price: float,
quantity: float,
take_profit_percentage: float | None = None,
stop_loss_percentage: float | None = None,
max_hold_time: int | None = None,
) -> "Position":
"""Create a position from a successful buy transaction.
Args:
mint: Token mint address
symbol: Token symbol
entry_price: Price at which position was entered
quantity: Quantity of tokens purchased
take_profit_percentage: Take profit percentage (0.5 = 50% profit)
stop_loss_percentage: Stop loss percentage (0.2 = 20% loss)
max_hold_time: Maximum hold time in seconds
Returns:
Position instance
"""
take_profit_price = None
if take_profit_percentage is not None:
take_profit_price = entry_price * (1 + take_profit_percentage)
stop_loss_price = None
if stop_loss_percentage is not None:
stop_loss_price = entry_price * (1 - stop_loss_percentage)
return cls(
mint=mint,
symbol=symbol,
entry_price=entry_price,
quantity=quantity,
entry_time=datetime.utcnow(),
take_profit_price=take_profit_price,
stop_loss_price=stop_loss_price,
max_hold_time=max_hold_time,
)
def should_exit(self, current_price: float) -> tuple[bool, ExitReason | None]:
"""Check if position should be exited based on current conditions.
Args:
current_price: Current token price
Returns:
Tuple of (should_exit, exit_reason)
"""
if not self.is_active:
return False, None
# Check take profit
if self.take_profit_price and current_price >= self.take_profit_price:
return True, ExitReason.TAKE_PROFIT
# Check stop loss
if self.stop_loss_price and current_price <= self.stop_loss_price:
return True, ExitReason.STOP_LOSS
# Check max hold time
if self.max_hold_time:
elapsed_time = (datetime.utcnow() - self.entry_time).total_seconds()
if elapsed_time >= self.max_hold_time:
return True, ExitReason.MAX_HOLD_TIME
return False, None
def close_position(self, exit_price: float, exit_reason: ExitReason) -> None:
"""Close the position with exit details.
Args:
exit_price: Price at which position was exited
exit_reason: Reason for exit
"""
self.is_active = False
self.exit_price = exit_price
self.exit_reason = exit_reason
self.exit_time = datetime.utcnow()
def get_pnl(self, current_price: float | None = None) -> dict:
"""Calculate profit/loss for the position.
Args:
current_price: Current price (uses exit_price if position is closed)
Returns:
Dictionary with PnL information
"""
if self.is_active and current_price is None:
raise ValueError("current_price required for active position")
price_to_use = self.exit_price if not self.is_active else current_price
if price_to_use is None:
raise ValueError("No price available for PnL calculation")
price_change = price_to_use - self.entry_price
price_change_pct = (price_change / self.entry_price) * 100
unrealized_pnl = price_change * self.quantity
return {
"entry_price": self.entry_price,
"current_price": price_to_use,
"price_change": price_change,
"price_change_pct": price_change_pct,
"unrealized_pnl_sol": unrealized_pnl,
"quantity": self.quantity,
}
def __str__(self) -> str:
"""String representation of position."""
if self.is_active:
status = "ACTIVE"
elif self.exit_reason:
status = f"CLOSED ({self.exit_reason.value})"
else:
status = "CLOSED (UNKNOWN)"
return f"Position({self.symbol}: {self.quantity:.6f} @ {self.entry_price:.8f} SOL - {status})"