← Tecnologia dos mercados financeiros: fundamentos e operação
09 / 10 · 60 MIN

Oficina: liquidação parcial e cancelamento pendente

Reconstruir parcelas, residuais e pedidos de manutenção sem transformar observações incompletas numa conclusão de negócio.

Começar pela obrigação

A aula usa uma instrução fictícia de 1500 unidades e 450001 cêntimos. O primeiro passo consiste em identificar serviço, conta, instrumento, referência da instrução e moeda. A mesma referência curta pode existir noutro âmbito; não basta procurar o primeiro registo com esse texto. Escreve numa folha o total original, o que está confirmado e o que falta explicar. Este registo serve ao L3 para delimitar o incidente e ao gestor para comunicar impacto. Nenhum campo do exercício representa uma instrução enviada a uma infraestrutura financeira real. São entradas controladas para praticar interpretação e reconciliação.

Separar parcelas e residuais

A primeira confirmação do exercício contém 900 unidades e 270000 cêntimos. Restam 600 unidades e 180001 cêntimos. O cêntimo adicional deve continuar visível, mesmo que o painel apresente euros sem casas decimais. A explicação pública do BCE sobre liquidação parcial distingue a parcela executada e o remanescente; o exercício usa os seus próprios números. Não implementa o algoritmo de seleção, janelas, mínimos ou regras de arredondamento de T2S. Recebe montantes já confirmados e verifica a conservação dos totais. Prever o residual antes de executar o código ajuda a detetar um erro de mapeamento que uma mensagem de sucesso não revelaria.

Pedido não é resultado

Enviar cancelamento não permite passar automaticamente as 600 unidades para canceladas. No fluxo bilateral consultado das GFS, uma dependência da contraparte pode manter o pedido pendente. Lê o estado da manutenção e o da instrução a que se refere, conservando as referências. No contrato local, só cancel-confirm transfere o remanescente para a parcela cancelada; as 900 unidades já liquidadas permanecem no histórico. A igualdade original = liquidado + cancelado + pendente deve continuar verdadeira. Se uma confirmação tardia entrar depois, a recusa do modelo exige reconciliação; não prova que o movimento externo seja inválido.

Interpretar dimensões de estado

O caso contém um snapshot completo com matched e dois holds. O seguinte remove PARTY, mas conserva CSD. O consumidor deve representar o hold restante. A orientação do BCE sobre múltiplos estados ajuda a perceber por que um único rótulo pronto pode esconder restrições. O programa não decide se uma infraestrutura pode liquidar: contabiliza observações e apresenta um sinal local de acompanhamento. Uma confirmação pode chegar depois de um estado que refletia outro instante. Para investigar, separa momento de origem, receção e processamento. Não elimines um movimento só porque o painel atual apresenta um hold cuja cronologia ainda não foi reconciliada.

Repetir sem duplicar efeitos

Copia o programa para uma pasta temporária e executa python3 run.py --output evidence.json com Python 3.13. O modelo reduz repetições idênticas por source/id e rejeita alterações de conteúdo com a mesma identidade. Também verifica movementRef: um novo message id não transforma um movimento antigo num segundo movimento. A função trabalha sobre cópias e deixa as entradas intactas perante falha. Esta propriedade em memória não demonstra persistência, transação distribuída ou recuperação depois de crash. Experimenta primeiro duplicar uma mensagem; depois muda apenas o seu id, mantendo a referência económica. Explica por que os dois resultados diferem.

Entregar uma conclusão verificável

Reserva quinze minutos para prever as parcelas, quinze para executar os casos e quinze para introduzir conflitos. Nos quinze finais, produz uma nota curta com obrigação original, parcelas confirmadas, residual, referências, incertezas, responsável e próxima atualização. Outra pessoa deve conseguir repetir a conta sem perguntar qual ficheiro escolheste. Confirma a versão aplicável ao ambiente: o índice consultado apresenta documentos R2026.NOV publicados em setembro e R2026.JUN como anteriores, o que não demonstra a ativação do teu ambiente. A conclusão do laboratório limita-se às regras locais e aos casos executados. A aceitação real exige contratos, sistemas e participantes autorizados.

"""Original DR post-trade observation exercise, Python 3.13.
Controlled inputs only; no T2S/DTCC messages, banking ledger or execution.
python3 run.py --output evidence.json
"""
import argparse
from copy import deepcopy
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import sys

checks=[]
def check(name,actual,expected):
    assert actual==expected,(name,actual,expected)
    checks.append(dict(name=name,actual=actual,expected=expected,passed=True))
def reject(fn):
    try:fn()
    except ValueError as e:return str(e)
    raise AssertionError('Expected rejection')
def positive(value):
    if type(value)is not int or value<=0:raise ValueError('positive integer required')
    return value
def unique(events):
    seen={};out=[]
    for e in events:
        key=(e['source'],e['id'])
        if key in seen:
            if seen[key]!=e:raise ValueError('message identity conflict')
            continue
        seen[key]=deepcopy(e);out.append(deepcopy(e))
    return out

def settlement(instruction,events):
    total=positive(instruction['quantity']);amount=positive(instruction['cashCents'])
    out=dict(original=total,settled=0,cancelled=0,pending=total,cashSettled=0,cashCancelled=0,cashPending=amount,matched=False,holds=[],cancelRequested=False,cancelConfirmed=False,movements=[])
    seen=set()
    for e in unique(events):
        if e['scope']!=instruction['scope']:raise ValueError('instruction scope mismatch')
        kind=e['kind']
        if kind=='status':
            if type(e['matched'])is not bool or not isinstance(e['holds'],list):raise ValueError('invalid status snapshot')
            out['matched']=e['matched'];out['holds']=sorted(set(e['holds']))
        elif kind=='cancel-request':out['cancelRequested']=True
        elif kind=='cancel-confirm':
            if out['cancelConfirmed']:raise ValueError('duplicate cancellation outcome')
            out['cancelConfirmed']=True;out['cancelRequested']=False
            out['cancelled']=out['pending'];out['cashCancelled']=out['cashPending']
            out['pending']=0;out['cashPending']=0
        elif kind=='settlement':
            if out['cancelConfirmed']:raise ValueError('settlement after cancellation needs reconciliation')
            if e['movementRef']in seen:raise ValueError('duplicate movement reference')
            qty=positive(e['quantity']);cash=positive(e['cashCents'])
            if qty>out['pending'] or cash>out['cashPending']:raise ValueError('exceeds remaining obligation')
            if (qty==out['pending'])!=(cash==out['cashPending']):raise ValueError('final quantity cash mismatch')
            seen.add(e['movementRef']);out['movements'].append(e['movementRef'])
            out['settled']+=qty;out['pending']-=qty;out['cashSettled']+=cash;out['cashPending']-=cash
        else:raise ValueError('unknown settlement observation')
    # Diagnostic local flag only; it does not command or permit settlement.
    out['localFollowUp']=out['pending']>0 and (bool(out['holds'])or not out['matched']or out['cancelRequested'])
    return out

def corporate(scope,events):
    out=dict(noticeRevision=None,preAdvice={},credits={},reversed=[],adjustments={},net=0)
    effect_refs=set()
    for e in unique(events):
        if e['scope']!=scope:raise ValueError('corporate scope mismatch')
        kind=e['kind']
        if kind=='notice':
            revision=positive(e['revision'])
            if out['noticeRevision'] is not None and revision<=out['noticeRevision']:raise ValueError('non increasing local notice revision')
            out['noticeRevision']=revision
        elif kind=='preadvice':out['preAdvice'][e['id']]=positive(e['amount'])
        elif kind=='cancel-preadvice':
            if e['ref']not in out['preAdvice']:raise ValueError('missing preadvice reference')
            del out['preAdvice'][e['ref']]
        elif kind in('credit','reversal','adjustment'):
            if e['effectRef']in effect_refs:raise ValueError('duplicate economic reference')
            if kind=='credit':
                value=positive(e['amount']);out['credits'][e['effectRef']]=value;out['net']+=value
            else:
                parent=e['ref']
                if parent not in out['credits']:raise ValueError('missing original credit')
                if parent in out['reversed']:raise ValueError('original credit already reversed')
                if kind=='reversal':
                    value=positive(e['amount'])
                    if value!=out['credits'][parent]:raise ValueError('reversal amount mismatch')
                    if any(a['ref']==parent for a in out['adjustments'].values()):raise ValueError('adjusted credit requires separate reconciliation')
                    out['reversed'].append(parent);out['net']-=value
                else:
                    value=e['amount']
                    if type(value)is not int or value==0:raise ValueError('nonzero integer adjustment required')
                    if out['net']+value<0:raise ValueError('outside nonnegative fixture scope')
                    out['adjustments'][e['effectRef']]=dict(ref=parent,amount=value);out['net']+=value
            effect_refs.add(e['effectRef'])
        else:raise ValueError('unknown corporate observation')
    return out

def deadline(received,limit):
    a=datetime.fromisoformat(received);b=datetime.fromisoformat(limit)
    if a.utcoffset()is None or b.utcoffset()is None:raise ValueError('explicit UTC offset required')
    return (a.astimezone(timezone.utc)<=b.astimezone(timezone.utc))

def main():
    sc=['DR-CSD','ACCOUNT-A','INSTRUMENT-X','INSTRUCTION-1','EUR']
    instruction=dict(scope=sc,quantity=1500,cashCents=450001)
    def se(id,kind,**extra):return dict(source='DR-OBS',id=id,scope=sc,kind=kind,**extra)
    status=se('s1','status',matched=True,holds=['CSD','PARTY'])
    part=se('s2','settlement',movementRef='M1',quantity=900,cashCents=270000)
    req=se('s3','cancel-request')
    events=[status,part,req];saved=deepcopy(events);a=settlement(instruction,events)
    check('partial quantity remains',a['pending'],600)
    check('partial cash remainder preserves cent',a['cashPending'],180001)
    check('request preserves settled quantity',a['settled'],900)
    check('request does not cancel remainder',a['cancelled'],0)
    check('request remains pending',a['cancelRequested'],True)
    check('matching does not erase holds',a['holds'],['CSD','PARTY'])
    check('partial request needs follow up',a['localFollowUp'],True)
    release=se('s4','status',matched=True,holds=['CSD'])
    check('one hold release retains another',settlement(instruction,[status,release])['holds'],['CSD'])
    b=settlement(instruction,events+[se('s5','cancel-confirm')])
    check('confirmed cancellation retains settled quantity',b['settled'],900)
    check('confirmed cancellation closes remaining quantity',b['cancelled'],600)
    check('confirmed cancellation retains cash remainder separately',b['cashCancelled'],180001)
    check('quantity conservation after cancellation',b['settled']+b['cancelled']+b['pending'],1500)
    check('cash conservation after cancellation',b['cashSettled']+b['cashCancelled']+b['cashPending'],450001)
    final=se('s6','settlement',movementRef='M2',quantity=600,cashCents=180001)
    complete=settlement(instruction,[part,final])
    check('final movement consumes residual cent',complete['cashSettled'],450001)
    check('complete quantity has no pending remainder',complete['pending'],0)
    check('identical message replay unchanged',settlement(instruction,[part,part]),settlement(instruction,[part]))
    check('changed payload with same identity rejected',reject(lambda:settlement(instruction,[part,{**part,'quantity':800}])),'message identity conflict')
    check('new message cannot reuse movement reference',reject(lambda:settlement(instruction,[part,{**part,'id':'new'}])),'duplicate movement reference')
    check('excess quantity rejected',reject(lambda:settlement(instruction,[{**part,'quantity':1501}])),'exceeds remaining obligation')
    check('final quantity with incomplete cash rejected',reject(lambda:settlement(instruction,[part,{**final,'cashCents':180000}])),'final quantity cash mismatch')
    check('wrong account scope rejected',reject(lambda:settlement(instruction,[{**part,'scope':['OTHER']}])),'instruction scope mismatch')
    check('late settlement after cancel held for reconciliation',reject(lambda:settlement(instruction,events+[se('s5','cancel-confirm'),final])),'settlement after cancellation needs reconciliation')
    check('input history preserved',events==saved,True)
    ca=['DR-CA','EVENT-1','ACCOUNT-A','INSTRUMENT-X','CASH','OPTION-1','EUR']
    def ce(id,kind,**extra):return dict(source='DR-OBS',id=id,scope=ca,kind=kind,**extra)
    notice=ce('a1','notice',revision=1);pre=ce('a2','preadvice',amount=7200)
    credit=ce('a3','credit',effectRef='C1',amount=7200)
    reversal=ce('a4','reversal',effectRef='R1',ref='C1',amount=7200)
    replacement=ce('a5','credit',effectRef='C2',amount=7500)
    check('announcement does not book cash',corporate(ca,[notice])['net'],0)
    check('preadvice does not book cash',corporate(ca,[notice,pre])['net'],0)
    check('cancel preadvice keeps net unchanged',corporate(ca,[pre,ce('a6','cancel-preadvice',ref='a2')])['net'],0)
    check('confirmed credit books amount',corporate(ca,[credit])['net'],7200)
    check('reversal removes original credit effect',corporate(ca,[credit,reversal])['net'],0)
    corrected=corporate(ca,[notice,pre,credit,ce('a7','notice',revision=2),reversal,replacement])
    check('replacement follows reversal without double count',corrected['net'],7500)
    check('original credit retained in history',corrected['credits']['C1'],7200)
    check('replayed credit counts once',corporate(ca,[credit,credit])['net'],7200)
    check('reversal before original is held',reject(lambda:corporate(ca,[reversal])),'missing original credit')
    check('mismatched reversal amount rejected',reject(lambda:corporate(ca,[credit,{**reversal,'amount':7100}])),'reversal amount mismatch')
    check('second reversal with new id rejected',reject(lambda:corporate(ca,[credit,reversal,{**reversal,'id':'a8','effectRef':'R2'}])),'original credit already reversed')
    check('same effect with new message rejected',reject(lambda:corporate(ca,[credit,{**credit,'id':'a9'}])),'duplicate economic reference')
    adj=ce('a10','adjustment',effectRef='D1',ref='C1',amount=-300)
    check('delta adjustment does not repay full value',corporate(ca,[credit,adj])['net'],6900)
    check('adjustment retains original credit',corporate(ca,[credit,adj])['credits']['C1'],7200)
    check('wrong corporate scope rejected',reject(lambda:corporate(ca,[{**credit,'scope':['OTHER']}])),'corporate scope mismatch')
    check('old notice revision does not overwrite new',reject(lambda:corporate(ca,[ce('a11','notice',revision=2),notice])),'non increasing local notice revision')
    check('offsets identify same deadline instant',deadline('2026-10-04T16:00:00+01:00','2026-10-04T15:00:00+00:00'),True)
    check('one second past deadline rejected locally',deadline('2026-10-04T16:00:01+01:00','2026-10-04T15:00:00+00:00'),False)
    check('naive time cannot prove deadline',reject(lambda:deadline('2026-10-04T16:00:00','2026-10-04T15:00:00+00:00')),'explicit UTC offset required')
    check('equal wall clocks can be different instants',deadline('2026-10-04T16:00:00+00:00','2026-10-04T16:00:00+01:00'),False)
    check('fractional local quantity rejected',reject(lambda:settlement(instruction,[{**part,'quantity':900.5}])),'positive integer required')
    check('reversal after adjustment needs separate reconciliation',reject(lambda:corporate(ca,[credit,adj,reversal])),'adjusted credit requires separate reconciliation')
    out=dict(scope='Original local observation reducers only; no T2S/DTCC protocol, settlement execution, bank ledger, legal calendar, actual instruction or independent acceptance.',python=sys.version.split()[0],runnerSha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),passed=len(checks),checks=checks)
    parser=argparse.ArgumentParser();parser.add_argument('--output',required=True);Path(parser.parse_args().output).write_text(json.dumps(out,indent=2)+'\n');print(json.dumps({'passed':len(checks)}))
if __name__=='__main__':main()
NA PRÁTICA

Caso fictício: 900 unidades liquidadas, 600 pendentes e um cancelamento pedido. O painel encerra a instrução cedo demais; a reconciliação conserva as parcelas e o residual de 180001 cêntimos.

Armadilhas comuns

Somar snapshots como movimentos; cancelar parcelas já liquidadas; perder residuais; interpretar matched como liquidação; confundir rejeição local com invalidade externa.

Tópicos relacionados: Sessões FIX e estado das ordens · Pós-negociação e liquidação · Operação, mudança e preparação T+1

Leva esta ideia contigo

Conservar a obrigação original, as parcelas e a incerteza permite explicar o resultado e decidir o próximo passo sem fabricar confirmações.

Criar conta

Referência: T2S General Functional Specifications R2026.JUN · DR financial markets technology professional assessment2026.10