| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { HttpError } from '../utils/apiResponse.js'; |
| import { positionsRepository } from './positions.repository.js'; |
| import { marketsRepository } from '../markets/markets.repository.js'; |
| import { signalsRepository } from '../signals/signals.repository.js'; |
| import { kellyFraction, suggestSize } from './kelly.js'; |
|
|
| function currentPriceForOutcome(market, outcome) { |
| return outcome === 'YES' ? market.yesPrice : market.noPrice; |
| } |
|
|
| function calcPnl(amountEur, entryPrice, currentPrice) { |
| if (!currentPrice || !entryPrice) return 0; |
| return amountEur * (currentPrice / entryPrice - 1); |
| } |
|
|
| export const positionsService = { |
| async open(userId, { marketId, outcome, amountEur }) { |
| const market = await marketsRepository.findById(marketId); |
| if (!market) throw new HttpError(404, 'NOT_FOUND', 'Market not found'); |
| if (market.status !== 'active') throw new HttpError(409, 'MARKET_CLOSED', 'Market is not active'); |
|
|
| const entryPrice = currentPriceForOutcome(market, outcome); |
| if (!entryPrice) throw new HttpError(409, 'NO_PRICE', 'Market price unavailable'); |
|
|
| const latestSignal = await signalsRepository.findLatestByMarket(marketId); |
| const confidence = latestSignal?.confidence ?? 0.5; |
| const fraction = kellyFraction(entryPrice, confidence); |
|
|
| return positionsRepository.create({ |
| userId, |
| marketId, |
| outcome, |
| amountEur, |
| entryPrice, |
| currentPrice: entryPrice, |
| pnl: 0, |
| kellyFraction: fraction, |
| status: 'open', |
| }); |
| }, |
|
|
| list(userId, status) { |
| return positionsRepository.findByUser(userId, status); |
| }, |
|
|
| async close(id, userId) { |
| const position = await positionsRepository.findByIdAndUser(id, userId); |
| if (!position) throw new HttpError(404, 'NOT_FOUND', 'Position not found'); |
| if (position.status === 'closed') throw new HttpError(409, 'ALREADY_CLOSED', 'Position already closed'); |
|
|
| const market = await marketsRepository.findById(position.marketId); |
| const currentPrice = market ? currentPriceForOutcome(market, position.outcome) : position.entryPrice; |
| const finalPnl = calcPnl(position.amountEur, position.entryPrice, currentPrice); |
|
|
| return positionsRepository.update(id, { |
| status: 'closed', |
| currentPrice, |
| pnl: finalPnl, |
| closedAt: new Date(), |
| }); |
| }, |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async suggest(marketId, bankroll = 1000) { |
| const market = await marketsRepository.findById(marketId); |
| if (!market) throw new HttpError(404, 'NOT_FOUND', 'Market not found'); |
|
|
| const signal = await signalsRepository.findLatestByMarket(marketId); |
|
|
| return suggestSize({ |
| yesPrice: market.yesPrice, |
| noPrice: market.noPrice, |
| spread: market.spread ?? 0, |
| signal, |
| bankroll, |
| }); |
| }, |
|
|
| async updateAllPnL() { |
| const open = await positionsRepository.findAllOpen(); |
| await Promise.all( |
| open.map((pos) => { |
| const currentPrice = currentPriceForOutcome(pos.market, pos.outcome); |
| if (!currentPrice) return Promise.resolve(); |
| return positionsRepository.update(pos.id, { |
| currentPrice, |
| pnl: calcPnl(pos.amountEur, pos.entryPrice, currentPrice), |
| }); |
| }), |
| ); |
| }, |
| }; |
|
|