-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Added arbitrage script * First version of arbitrage agent * Added test for Arbitrage agent * Updated poetry.lock * Fixed mypy * Last fixes before review * Removed unnecessary tokens from prompt * Now handling correlation output as a bool instead of a float between [-1,1] * Making sure agent only runs for Omen * Clean up * Addressed PR comments * Fixing tests * Adding pinecone update when running Arbitrage agent * Betting in more markets * Update prediction_market_agent/agents/arbitrage_agent/data_models.py Co-authored-by: Peter Jung <[email protected]> * Update tests/agents/arbitrage_agent/test_arbitrage_agent.py Co-authored-by: Peter Jung <[email protected]> * Implemented Pinecone upgrades * Removed lower when fetching markets * Pinecone adjustments - now uses only open markets for correlation * Handling case corr < 0 * Added tests to data models * Final touches * Merged lock * Updated lock * Filtering only open markets on Arbitrage agent * Update prediction_market_agent/db/pinecone_handler.py Co-authored-by: Peter Jung <[email protected]> * Reverted some changes in the pinecone_handler.py * Fixed ci * Filtering omen markets earlier * Changes required due to new PMAT version * Fixing merge conflicts --------- Co-authored-by: Peter Jung <[email protected]>
- Loading branch information
1 parent
7122b9c
commit 4b3d1c0
Showing
17 changed files
with
977 additions
and
522 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
93 changes: 93 additions & 0 deletions
93
prediction_market_agent/agents/arbitrage_agent/data_models.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import typing as t | ||
|
||
from prediction_market_agent_tooling.markets.agent_market import AgentMarket | ||
from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet | ||
from prediction_market_agent_tooling.tools.utils import check_not_none | ||
from pydantic import BaseModel | ||
|
||
|
||
class Correlation(BaseModel): | ||
near_perfect_correlation: bool | None | ||
reasoning: str | ||
|
||
|
||
class ArbitrageBet(BaseModel): | ||
main_market_bet: SimpleBet | ||
related_market_bet: SimpleBet | ||
|
||
|
||
class CorrelatedMarketPair(BaseModel): | ||
main_market: AgentMarket | ||
related_market: AgentMarket | ||
correlation: Correlation | ||
|
||
def __str__(self) -> str: | ||
return f"main_market {self.main_market.question} related_market_question {self.related_market.question} potential_profit_per_unit {self.potential_profit_per_bet_unit()}" | ||
|
||
def potential_profit_per_bet_unit(self) -> float: | ||
""" | ||
Calculate potential profit per bet unit based on high positive market correlation. | ||
For positively correlated markets: Bet YES/NO or NO/YES. | ||
""" | ||
|
||
if self.correlation.near_perfect_correlation is None: | ||
return 0 | ||
|
||
bet_direction_main, bet_direction_related = self.bet_directions() | ||
p_main = ( | ||
self.main_market.current_p_yes | ||
if bet_direction_main | ||
else self.main_market.current_p_no | ||
) | ||
p_related = ( | ||
self.related_market.current_p_yes | ||
if bet_direction_related | ||
else self.related_market.current_p_no | ||
) | ||
denominator = p_main + p_related | ||
return (1 / denominator) - 1 | ||
|
||
def bet_directions(self) -> t.Tuple[bool, bool]: | ||
correlation = check_not_none(self.correlation.near_perfect_correlation) | ||
if correlation: | ||
# We compare denominators for cases YES/NO and NO/YES bets and take the most profitable (i.e. where denominator is the lowest). | ||
# For other cases we employ similar logic. | ||
yes_no = self.main_market.current_p_yes + self.related_market.current_p_no | ||
no_yes = self.main_market.current_p_no + self.related_market.current_p_yes | ||
return (True, False) if yes_no <= no_yes else (False, True) | ||
|
||
else: | ||
yes_yes = self.main_market.current_p_yes + self.related_market.current_p_yes | ||
no_no = self.main_market.current_p_no + self.related_market.current_p_no | ||
return (True, True) if yes_yes <= no_no else (False, False) | ||
|
||
def split_bet_amount_between_yes_and_no( | ||
self, total_bet_amount: float | ||
) -> ArbitrageBet: | ||
"""Splits total bet amount following equations below: | ||
A1/p1 = A2/p2 (same profit regardless of outcome resolution) | ||
A1 + A2 = total bet amount | ||
""" | ||
|
||
bet_direction_main, bet_direction_related = self.bet_directions() | ||
|
||
p_main = ( | ||
self.main_market.current_p_yes | ||
if bet_direction_main | ||
else self.main_market.current_p_no | ||
) | ||
p_related = ( | ||
self.related_market.current_p_yes | ||
if bet_direction_related | ||
else self.related_market.current_p_no | ||
) | ||
total_probability = p_main + p_related | ||
bet_main = total_bet_amount * p_main / total_probability | ||
bet_related = total_bet_amount * p_related / total_probability | ||
main_market_bet = SimpleBet(direction=bet_direction_main, size=bet_main) | ||
related_market_bet = SimpleBet( | ||
direction=bet_direction_related, size=bet_related | ||
) | ||
return ArbitrageBet( | ||
main_market_bet=main_market_bet, related_market_bet=related_market_bet | ||
) |
190 changes: 190 additions & 0 deletions
190
prediction_market_agent/agents/arbitrage_agent/deploy.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
import typing as t | ||
|
||
from langchain_core.output_parsers import PydanticOutputParser | ||
from langchain_core.prompts import PromptTemplate | ||
from langchain_core.runnables import RunnableSerializable | ||
from langchain_openai import ChatOpenAI | ||
from prediction_market_agent_tooling.deploy.agent import DeployableTraderAgent | ||
from prediction_market_agent_tooling.gtypes import Probability | ||
from prediction_market_agent_tooling.loggers import logger | ||
from prediction_market_agent_tooling.markets.agent_market import AgentMarket | ||
from prediction_market_agent_tooling.markets.data_models import ( | ||
BetAmount, | ||
Position, | ||
ProbabilisticAnswer, | ||
TokenAmount, | ||
Trade, | ||
TradeType, | ||
) | ||
from prediction_market_agent_tooling.markets.markets import MarketType | ||
from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket | ||
from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import ( | ||
OmenSubgraphHandler, | ||
) | ||
from prediction_market_agent_tooling.tools.langfuse_ import ( | ||
get_langfuse_langchain_config, | ||
observe, | ||
) | ||
from prediction_market_agent_tooling.tools.utils import utcnow | ||
|
||
from prediction_market_agent.agents.arbitrage_agent.data_models import ( | ||
CorrelatedMarketPair, | ||
Correlation, | ||
) | ||
from prediction_market_agent.agents.arbitrage_agent.prompt import PROMPT_TEMPLATE | ||
from prediction_market_agent.db.pinecone_handler import PineconeHandler | ||
from prediction_market_agent.utils import APIKeys | ||
|
||
|
||
class DeployableArbitrageAgent(DeployableTraderAgent): | ||
"""Agent that places mirror bets on Omen for (quasi) risk-neutral profit.""" | ||
|
||
model = "gpt-4o" | ||
# trade amount will be divided between correlated markets. | ||
total_trade_amount = BetAmount(amount=0.1, currency=OmenAgentMarket.currency) | ||
bet_on_n_markets_per_run = 5 | ||
max_related_markets_per_market = 10 | ||
n_markets_to_fetch = 50 | ||
|
||
def run(self, market_type: MarketType) -> None: | ||
if market_type != MarketType.OMEN: | ||
raise RuntimeError( | ||
"Can arbitrage only on Omen since related markets embeddings available only for Omen markets." | ||
) | ||
self.subgraph_handler = OmenSubgraphHandler() | ||
self.pinecone_handler = PineconeHandler() | ||
self.pinecone_handler.insert_all_omen_markets_if_not_exists() | ||
self.chain = self._build_chain() | ||
super().run(market_type=market_type) | ||
|
||
def answer_binary_market(self, market: AgentMarket) -> ProbabilisticAnswer | None: | ||
return ProbabilisticAnswer(p_yes=Probability(0.5), confidence=1.0) | ||
|
||
def _build_chain(self) -> RunnableSerializable[t.Any, t.Any]: | ||
llm = ChatOpenAI( | ||
temperature=0, | ||
model=self.model, | ||
api_key=APIKeys().openai_api_key_secretstr_v1, | ||
) | ||
|
||
parser = PydanticOutputParser(pydantic_object=Correlation) | ||
prompt = PromptTemplate( | ||
template=PROMPT_TEMPLATE, | ||
input_variables=["main_market_question", "related_market_question"], | ||
partial_variables={"format_instructions": parser.get_format_instructions()}, | ||
) | ||
|
||
chain = prompt | llm | parser | ||
return chain | ||
|
||
@observe() | ||
def calculate_correlation_between_markets( | ||
self, market: AgentMarket, related_market: AgentMarket | ||
) -> Correlation: | ||
correlation: Correlation = self.chain.invoke( | ||
{ | ||
"main_market_question": market.question, | ||
"related_market_question": related_market.question, | ||
} | ||
) | ||
return correlation | ||
|
||
@observe() | ||
def get_correlated_markets(self, market: AgentMarket) -> list[CorrelatedMarketPair]: | ||
# We try to find similar, open markets which point to the same outcome. | ||
correlated_markets = [] | ||
# We only wanted to find related markets that are open. | ||
# We intentionally query more markets in the hope it yields open markets. | ||
# We could store market_status (open, closed) in Pinecone, but we refrain from it | ||
# to keep the chain data (or graph) as the source-of-truth, instead of managing the | ||
# update process of the vectorDB. | ||
|
||
related = self.pinecone_handler.find_nearest_questions_with_threshold( | ||
limit=self.max_related_markets_per_market, | ||
text=market.question, | ||
filter_on_metadata={ | ||
"close_time_timestamp": {"gte": utcnow().timestamp() + 3600} | ||
}, # closing 1h from now | ||
) | ||
|
||
omen_markets = self.subgraph_handler.get_omen_binary_markets( | ||
limit=len(related), | ||
id_in=[i.market_address for i in related if i.market_address != market.id], | ||
resolved=False, | ||
) | ||
|
||
# Order omen_markets in the same order as related | ||
related_market_addresses = [i.market_address for i in related] | ||
omen_markets = sorted( | ||
omen_markets, key=lambda m: related_market_addresses.index(m.id) | ||
) | ||
|
||
print(f"Fetched {len(omen_markets)} related markets for market {market.id}") | ||
|
||
for related_market in omen_markets: | ||
result: Correlation = self.chain.invoke( | ||
{ | ||
"main_market_question": market, | ||
"related_market_question": related_market, | ||
}, | ||
config=get_langfuse_langchain_config(), | ||
) | ||
if result.near_perfect_correlation is not None: | ||
related_agent_market = OmenAgentMarket.from_data_model(related_market) | ||
correlated_markets.append( | ||
CorrelatedMarketPair( | ||
main_market=market, | ||
related_market=related_agent_market, | ||
correlation=result, | ||
) | ||
) | ||
return correlated_markets | ||
|
||
@observe() | ||
def build_trades_for_correlated_markets( | ||
self, pair: CorrelatedMarketPair | ||
) -> list[Trade]: | ||
# Split between main_market and related_market | ||
arbitrage_bet = pair.split_bet_amount_between_yes_and_no( | ||
self.total_trade_amount.amount | ||
) | ||
|
||
main_trade = Trade( | ||
trade_type=TradeType.BUY, | ||
outcome=arbitrage_bet.main_market_bet.direction, | ||
amount=TokenAmount( | ||
amount=arbitrage_bet.main_market_bet.size, | ||
currency=pair.main_market.currency, | ||
), | ||
) | ||
|
||
# related trade | ||
related_trade = Trade( | ||
trade_type=TradeType.BUY, | ||
outcome=arbitrage_bet.related_market_bet.direction, | ||
amount=TokenAmount( | ||
amount=arbitrage_bet.related_market_bet.size, | ||
currency=pair.related_market.currency, | ||
), | ||
) | ||
|
||
trades = [main_trade, related_trade] | ||
logger.info(f"Placing arbitrage trades {trades}") | ||
return trades | ||
|
||
@observe() | ||
def build_trades( | ||
self, | ||
market: AgentMarket, | ||
answer: ProbabilisticAnswer, | ||
existing_position: Position | None, | ||
) -> list[Trade]: | ||
trades = [] | ||
correlated_markets = self.get_correlated_markets(market=market) | ||
for pair in correlated_markets: | ||
# We want to profit at least 0.5% per market (value chosen as initial baseline). | ||
if pair.potential_profit_per_bet_unit() > 0.005: | ||
trades_for_pair = self.build_trades_for_correlated_markets(pair) | ||
trades.extend(trades_for_pair) | ||
|
||
return trades |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
PROMPT_TEMPLATE = """Given two markets, MARKET 1 and MARKET 2, provide a boolean value that represents the correlation between these two markets' outcomes. Return True if the outcomes are perfectly or nearly perfectly correlated, meaning there is a high probability that both markets resolve to the same outcome. Return False if the outcomes are perfectly or nearly perfectly inversely correlated, and finally return None if the correlation is weak or non-existent. | ||
Correlation can also be understood as the conditional probability that market 2 resolves to YES, given that market 1 resolved to YES. | ||
In addition to the correlation value, explain the reasoning behind your decision. | ||
[MARKET 1] | ||
{main_market_question} | ||
[MARKET 2] | ||
{related_market_question} | ||
Follow the formatting instructions below for producing an output in the correct format. | ||
{format_instructions}""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.