Files
tradon/modules/purchase_trade/derivative.py
2025-12-26 13:11:43 +00:00

402 lines
13 KiB
Python

import datetime
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
from sql import Literal
from sql.functions import CurrentTimestamp
from trytond.wizard import Button, StateTransition, StateView, Wizard
from trytond.pyson import Bool, Eval, Id, If
class DerivativeMatch(ModelSQL, ModelView):
"Derivative Match"
__name__ = 'derivative.match'
long_position = fields.Many2One('derivative.derivative', 'Long Position', required=True)
short_position = fields.Many2One('derivative.derivative', 'Short Position', required=True)
quantity = fields.Numeric('Matched Quantity', digits='unit', required=True)
match_date = fields.Date('Match Date', required=True)
method = fields.Selection([
('fifo', 'FIFO'),
('lifo', 'LIFO'),
('manual', 'Manual'),
], 'Method', required=True)
pnl = fields.Function(fields.Numeric('PnL', digits='currency'), 'get_pnl')
def get_pnl(self, name):
return (self.short_position.price - self.long_position.price) * self.quantity
class Derivative(ModelSQL,ModelView):
"Derivative"
__name__ = 'derivative.derivative'
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Purch. Line")
efp = fields.Boolean("EFP")
price_index = fields.Many2One('price.price',"Curve")
nb_ct = fields.Integer("Nb ct")
price = fields.Numeric("Entry price",digits='currency')
exit_price = fields.Numeric("Exit price",digits='currency')
product = fields.Many2One('product.product',"Product")
party = fields.Many2One('party.party','Supplier')
currency = fields.Function(fields.Many2One('currency.currency',"Cur"),'get_cur')
quantity = fields.Function(fields.Numeric("Quantity",digits='unit'),'get_qt')
alloc_qty = fields.Function(fields.Numeric("Alloc. Qty",digits='unit'),'get_alloc_qt')
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
amount = fields.Function(fields.Numeric("Amount",digits='currency'),'get_amount')
currency2 = fields.Function(fields.Many2One('currency.currency',"Cur"),'get_cur2')
trade_date = fields.Date('Trade Date')
maturity_date = fields.Date('Maturity')
open_qty = fields.Numeric('Open Quantity', digits='unit')
matches_long = fields.One2Many('derivative.match', 'long_position', 'Matches (Long)', states={'invisible': (Eval('direction') == 'long')})
matches_short = fields.One2Many('derivative.match', 'short_position', 'Matches (Short)', states={'invisible': (Eval('direction') == 'short')})
direction = fields.Selection([
('long', 'Long'),
('short', 'Short'),
], 'Direction', required=True)
state = fields.Selection([
('open', 'Open'),
('closed', 'Closed'),
], 'State', required=True)
@classmethod
def default_state(cls):
return 'open'
def get_amount(self,name):
if self.price_index and self.line and self.price and self.nb_ct:
#pi = Pool().get('price.price')(self.price_index)
return self.price_index.get_amount_nb_ct(self.price,self.nb_ct,self.line.unit,self.line.purchase.currency if self.line.purchase else self.sale_line.sale.currency)
def get_cur(self,name):
if self.price_index:
#pi = Pool().get('price.price')(self.price_index)
return self.price_index.price_currency
def get_cur2(self,name):
if self.purchase:
return self.purchase.currency
def get_unit(self,name):
if self.line:
return self.line.unit
def get_qt(self,name):
if self.line:
line = self.line
else:
if self.purchase and self.purchase.lines:
line = self.purchase.lines[0]
else:
line = None
if self.price_index and self.nb_ct and line:
return self.price_index.get_qt(self.nb_ct,line.unit)
def get_alloc_qt(self,name):
if self.line:
line = self.line
else:
if self.purchase and self.purchase.lines:
line = self.purchase.lines[0]
else:
line = None
if self.price_index and self.nb_ct and line:
if line.price_summary:
return line.price_summary[0].fixed_qt
class MatchWizardStart(ModelView):
"Match Selected Derivatives"
__name__ = 'derivative.match.start'
method = fields.Selection([
('fifo', 'FIFO'),
('lifo', 'LIFO'),
('manual', 'Manual'),
], 'Method', required=True)
quantity = fields.Numeric(
'Quantity to Match',
digits='unit',
required=True
)
class DerivativeMatchWizard(Wizard):
"Derivative Match Wizard"
__name__ = 'derivative.match.wizard'
start = StateView(
'derivative.match.start',
'purchase_trade.derivative_match_start_form_view',
[
Button('Cancel', 'end', 'tryton-cancel'),
Button('Match', 'match', 'tryton-ok', default=True),
]
)
match = StateTransition()
def transition_match(self):
pool = Pool()
Derivative = pool.get('derivative.derivative')
Date = pool.get('ir.date')
Match = pool.get('derivative.match')
active_ids = Transaction().context.get('active_ids', [])
if not active_ids:
return 'end'
positions = Derivative.browse(active_ids)
longs = [
p for p in positions
if p.direction == 'long'
and p.state == 'open'
and p.open_qty > 0
]
shorts = [
p for p in positions
if p.direction == 'short'
and p.state == 'open'
and p.open_qty > 0
]
if not longs or not shorts:
return 'end'
# FIFO / LIFO ordering
reverse = self.start.method == 'lifo'
longs.sort(key=lambda p: p.trade_date or datetime.date.min, reverse=reverse)
shorts.sort(key=lambda p: p.trade_date or datetime.date.min, reverse=reverse)
remaining_qty = self.start.quantity
for long_pos in longs:
if remaining_qty <= 0:
break
for short_pos in shorts:
if remaining_qty <= 0:
break
match_qty = min(
long_pos.open_qty,
short_pos.open_qty,
remaining_qty
)
if match_qty <= 0:
continue
Match.create([{
'long_position': long_pos.id,
'short_position': short_pos.id,
'quantity': match_qty,
'method': self.start.method,
'match_date': Date.today(),
}])
long_pos.open_qty -= match_qty
short_pos.open_qty -= match_qty
remaining_qty -= match_qty
if long_pos.open_qty == 0:
long_pos.state = 'closed'
if short_pos.open_qty == 0:
short_pos.state = 'closed'
long_pos.save()
short_pos.save()
return 'end'
class DerivativeReport(ModelSQL, ModelView):
"Derivative Position Report"
__name__ = 'derivative.report'
r_derivative = fields.Many2One(
'derivative.derivative',
"Derivative"
)
r_trade_date = fields.Date("Trade Date")
r_maturity_date = fields.Date("Maturity")
r_product = fields.Many2One(
'product.product',
"Product"
)
r_party = fields.Many2One(
'party.party',
"Counterparty"
)
r_direction = fields.Selection([
(None, ''),
('long', 'Long'),
('short', 'Short'),
], 'Direction')
r_state = fields.Selection([
('open', 'Open'),
('closed', 'Closed'),
], 'State')
r_quantity = fields.Function(fields.Numeric("Initial Qty",digits='unit'),'get_qt')
r_open_qty = fields.Numeric(
"Open Qty",
digits='unit'
)
r_price = fields.Numeric(
"Entry Price",
digits='currency'
)
r_currency = fields.Function(fields.Many2One('currency.currency',"Currency"),'get_cur')
def get_cur(self,name):
if self.r_derivative:
if self.r_derivative.price_index:
return self.r_derivative.price_index.price_currency
def get_qt(self,name):
if self.r_derivative:
if self.r_derivative.line:
line = self.r_derivative.line
else:
if self.r_derivative.purchase and self.r_derivative.purchase.lines:
line = self.r_derivative.purchase.lines[0]
else:
line = None
if self.r_derivative.price_index and self.r_derivative.nb_ct and line:
return self.r_derivative.price_index.get_qt(self.r_derivative.nb_ct,line.unit)
# ------------------------------------------------------------
# TABLE QUERY
# ------------------------------------------------------------
@classmethod
def table_query(cls):
pool = Pool()
Derivative = pool.get('derivative.derivative')
d = Derivative.__table__()
context = Transaction().context
product = context.get('product')
party = context.get('party')
direction = context.get('direction')
state = context.get('state')
trade_from = context.get('trade_from')
trade_to = context.get('trade_to')
maturity_from = context.get('maturity_from')
maturity_to = context.get('maturity_to')
open_only = context.get('open_only')
wh = Literal(True)
if product:
wh &= (d.product == product)
if party:
wh &= (d.party == party)
if direction:
wh &= (d.direction == direction)
if state:
wh &= (d.state == state)
if open_only:
wh &= (d.open_qty > 0)
if trade_from:
wh &= (d.trade_date >= trade_from)
if trade_to:
wh &= (d.trade_date <= trade_to)
if maturity_from:
wh &= (d.maturity_date >= maturity_from)
if maturity_to:
wh &= (d.maturity_date <= maturity_to)
query = d.select(
# mandatory technical fields
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
d.id.as_('id'),
d.id.as_('r_derivative'),
d.trade_date.as_('r_trade_date'),
d.maturity_date.as_('r_maturity_date'),
d.product.as_('r_product'),
d.party.as_('r_party'),
d.direction.as_('r_direction'),
d.state.as_('r_state'),
#d.quantity.as_('r_quantity'),
d.open_qty.as_('r_open_qty'),
d.price.as_('r_price'),
where=wh
)
return query
# ------------------------------------------------------------
# SEARCH NAME
# ------------------------------------------------------------
@classmethod
def search_rec_name(cls, name, clause):
_, operator, operand, *extra = clause
return [
'OR',
('r_product', operator, operand, *extra),
('r_party', operator, operand, *extra),
]
class DerivativeReportContext(ModelView):
"Derivative Report Context"
__name__ = 'derivative.report.context'
trade_from = fields.Date("Trade Date From")
trade_to = fields.Date("Trade Date To")
maturity_from = fields.Date("Maturity From")
maturity_to = fields.Date("Maturity To")
product = fields.Many2One(
'product.product',
"Product"
)
party = fields.Many2One(
'party.party',
"Counterparty"
)
direction = fields.Selection([
('long', 'Long'),
('short', 'Short'),
], 'Direction')
state = fields.Selection([
('open', 'Open'),
('closed', 'Closed'),
], 'State')
open_only = fields.Boolean("Open Positions Only")
@classmethod
def default_trade_from(cls):
return datetime.date(1999, 1, 1)
@classmethod
def default_trade_to(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()