D30-F04-A04 / Released engineering topic

FX Revaluation Posting: EUR Monetary Positions into Functional USD with Exact Rate Evidence

Calculate a closing-rate target, subtract current carrying and propose balanced functional-USD legs while preserving foreign quantity and known-at evidence.

FX Revaluation Posting: EUR Monetary Positions into Functional USD with Exact Rate EvidenceD30 / D30-F04

At period close, an operator needs to explain the difference between a foreign monetary position's current functional carrying value and its value at the closing rate. That explanation must identify the rate, its direction, the source position and the exact adjustment. Fetching a current quote and multiplying a foreign ledger total does not establish those facts.

This tutorial builds a narrow, reproducible calculation for an ordinary EUR monetary asset or liability measured into functional USD. It keeps the native EUR quantity unchanged and produces a USD posting proposal against current evidenced carrying. It does not commit a journal or attest that a close is complete.

You will calculate the target and adjustment, explain asset-versus-liability signs, invert quotes exactly, preserve the knowledge boundary around a revised rate and verify complete Python/TypeScript outputs. All Cedar positions and quotes are synthetic, including the forward-dated October close. No actual market fixing or completed close is claimed.

Trial-Balance Validation establishes the scope, currency and sign boundary for current carrying. Idempotent Journal Posting explains why an accepted business operation must replay its stored result. Those are separate responsibilities from calculating a new measurement.

Define the measured object before the formula

The initial profile accepts a positive EUR monetary quantity and a nonnegative USD carrying magnitude for the same position. Asset or liability nature supplies the sign interpretation. The extracted raw balance must be positive for an asset and negative for a liability, with zero represented consistently.

The host supplies the monetary classification, functional currency, exchangeability and exception assertions. The calculator checks their strict representation and routes unsupported cases outside the profile; it does not determine those accounting facts from an account name. IAS 21's ordinary closing-rate monetary measurement and exchange-difference principles are verified at paragraphs 16, 23(a) and 28 through the first-party AASB 121 adoption.

This profile excludes nonmonetary items, hedge accounting, net-investment exceptions, hyperinflation, nonexchangeability and whole-operation presentation-currency translation. It also requires quantity and carrying to describe the same basis. A gross foreign notional and an unrelated net allowance are not interchangeable inputs.

Two views of a position are involved: a native quantity record and a functional valuation record. The proposed account is a USD functional valuation account associated with that position. It is not a USD entry posted to the native cash_eur account, and the EUR quantity is not added to a USD journal total. Separate gain and loss accounts have income and expense classifications respectively.

Keep the units visible

Let q be EUR minor units and r be USD major units per one EUR major unit. EUR and USD each have 100 minor units per major unit in this explicit profile:

Texact=(q100  EUR)×(r  USDEUR)×(100  USD minorUSD)=q×r  USD minor.T_{exact}=\left(\frac{q}{100}\;\text{EUR}\right) \times\left(r\;\frac{\text{USD}}{\text{EUR}}\right) \times\left(100\;\frac{\text{USD minor}}{\text{USD}}\right) =q\times r\;\text{USD minor}.

The two factors of 100 cancel because the chosen currency exponents agree. That cancellation is not a general rule for arbitrary currencies.

Compute the exact target, round that target once using the declared half-even policy, then subtract current carrying C:

T=halfEven⁡(Texact),Δ=T−C.T=\operatorname{halfEven}(T_{exact}),\qquad \Delta=T-C.
SymbolMeaning
qPositive native EUR quantity in minor units
rExact USD-major-per-EUR-major closing rate
CCurrent evidenced USD carrying magnitude, in minor units
TRounded USD target, in minor units
ΔSigned change in functional carrying, T minus C

Half-even target rounding is this implementation's explicit policy. We do not claim that IAS 21 prescribes this JSON representation or rounding rule. All rates and intermediate values remain reduced rational fractions; decimals are display aids.

Work the carrying-value bridge

In a synthetic example, q is 100000 EUR minor units, or EUR 1000.00. Current carrying C is 108000 USD minor units, or USD 1080.00. A quote of 11/10 USD per EUR produces a target of 110000 USD minor units. The adjustment is therefore 2000 USD minor units.

For an asset, the increase produces the proposal:

Plain text
Debit  USD monetary asset valuation     2000
Credit FX gain                         2000

For a liability, the same increase in carrying produces a different result:

Plain text
Debit  FX loss                         2000
Credit USD monetary liability valuation 2000

The native quantity remains 100000 EUR minor units in both cases. A valuation adjustment is not a new EUR receipt or payment.

The carrying-value bridge keeps the native quantity and functional adjustment separate

Open the full-size carrying-value diagram.

When the target falls, the directions reverse. An asset decrease debits FX loss and credits valuation; a liability decrease debits valuation and credits FX gain. A zero delta emits no legs. The account catalog checks the requested entity, ledger, USD currency, active status and asset/liability/income/expense roles before producing a proposal.

Round the target, not the difference

A small independent example exposes an easy implementation error. Suppose q is 3 EUR minor units and r is 1/2 USD per EUR. The exact target is 1.5 USD minor units. Half-even rounds that target to 2. If current carrying is 1, the adjustment is 1.

Rounding the difference first would calculate 1.5 − 1 = 0.5 and round it to zero. That produces a different journal. Keep the ordering explicit in code and tests.

The reference also retains exact_target − rounded_target as a signed diagnostic fraction. The 1.5 example leaves −1/2 minor unit; a 0.5 target rounds to zero and leaves +1/2. This residual is not interest carry, a new foreign quantity or a balance to distribute across unrelated positions.

Invert a stated quote exactly

A quote must name its base and quote currencies. The forward quote 11/10 USD per EUR and the inverse quote 10/11 EUR per USD describe the same economic rate. The reference normalizes either accepted orientation to USD per EUR using exact inversion.

Multiplying q by the inverse quote without inversion would measure the wrong units. A bare decimal such as 1.10 does not identify direction, measurement date, availability or source. Unsupported currency pairs and malformed/unreduced rational wires reject; the code does not infer a quote from a provider ticker or choose between bid and ask.

Separate measurement from knowledge

The profile fixes measurement at the last UTC millisecond of the supplied calendar month. This is a declared teaching/host convention, not a universal market fixing time. The complete rate history refers to that one measurement point, while each revision has its own availability receipt.

Consider a modeled 31 October close. Version 1, rate 11/10, becomes available at 08:00 on 1 November. Version 2, rate 111/100, arrives at 08:00 on 2 November. At noon on 1 November, select version 1. At version 2's receipt equality, select version 2. For q = 100000, the target changes from 110000 to 111000 USD minor units; current carrying still determines the adjustment.

Explicit quote inversion and inclusive rate-receipt selection

Open the full-size quote and knowledge diagram.

Validate the entire normalized revision chain against an independent trusted head before selecting K. Delivery order can vary, but declared revisions must cover the head without gaps or conflicting identities. Future malformed or omitted history cannot be hidden behind the earlier cutoff. Successful coverage may describe a later validated revision while the selected rate and calculation contain only the revision available by K.

If no closing version is visible, return an indeterminate result. Do not substitute a nearby date or a live quote. Missing evidence is also different from a nonexchangeable currency, which requires another accounting method. The 2023 exchangeability amendment became effective for annual periods beginning in 2025; this initial profile deliberately routes that method outside. The 2025 hyperinflationary presentation amendment has a 2027 effective date with early application possible, and its different translation problem is also excluded.

Implement the strict source and calculation contract

The API takes request, position snapshot, rate history and context. The trusted position receipt binds entity/ledger, period, functional-USD basis, knowledge cutoff, observation time, visible whole-group journal position at K and exact position/current-carrying fields. Separate source coverage may validate a later journal head; that later position must not be mislabeled as visible at K. The independent rate head binds complete rate coverage. Strict parsing checks these assertions; it does not authenticate them cryptographically.

The host adapter must bind the extracted receipt to actual trial-balance/current position evidence. A receipt ID alone does not establish completeness, current carrying or the absence of undisclosed journals. The exact data contract lists every field, bound, supported state and error precedence.

Plain text
validate request/profile and extracted position receipt
check current carrying/raw sign and mapped USD account roles
validate the full rate history against its independent head
select the latest revision available by K and normalize quote direction
calculate exact target, round target, subtract current carrying
return calculation and zero/ready/blocked functional-USD proposal

The self-contained Python reference uses Fraction and integers. The TypeScript reference uses reduced BigInt rationals. Neither imports a sibling tutorial or performs I/O. Failures withhold calculation and proposal branches rather than returning a plausible partial amount.

Quantity and rate parts have at most 30 digits. Current carrying and target have a 60-digit profile. Only an individual adjustment beyond 10^30 − 1 minor units blocks the proposed legs. A large resulting balance is not itself a Stage1 leg-limit violation. The exact target and delta remain available when a proposal is blocked; they are never truncated into a valid-looking journal.

Explore and verify the actual reference

The guided FX lab retains 24 independent synthetic positions. It is a casebook, not a real aggregated portfolio. Step changes focus while every case remains visible. Compare forward and inverse quotes, future versus known revision, asset versus liability nature, small half-even quantities and oversized posting legs.

The hypothetical updated-carrying scenario supplies a new receipt with C equal to the earlier target. Recalculation then returns zero. That scenario models an input after acceptance; the calculator has not performed the acceptance. Missing-rate and nonmonetary scenarios withhold the calculation entirely.

Validation compares 107 independently authored complete expected outputs with both implementations, including 14 literal math cases fixed before code. The suite covers exact signs and fractions, target-rounding order, revision equality, complete 512-version history, missing/future malformed evidence, strict shapes, unsupported exceptions, large targets, leg limits and detached mutable output views. Browser QA exercises 510 meaningful observations across phone, tablet and desktop widths, and compares all captured complete outputs against Python. The validation record contains commands, receipts and screenshots.

These checks establish behavior under the declared synthetic contract. They do not establish source authenticity, operational performance or complete financial-reporting compliance. A historical position example is not useful without licensed quantity/carrying snapshots, known-at rate versions and applicable accounting decisions. A public aggregate FX figure cannot reproduce those details.

Carry the proposal into controlled acceptance

After an accepted adjustment is reflected in a fresh current carrying receipt, the same measurement can calculate zero. This is a measurement property, not a replacement for durable idempotency. Replaying the original business operation must return its stored posting result before current-state revalidation. A changed payload under a reused identity still conflicts.

Likewise, successful nonzero calculation does not mean the FX close prerequisite passed. A trusted control runner must bind actual accepted posting/current carrying, with the extracted visible journal position equal to the frozen current close head. A historical extraction behind that head cannot certify the current close. Period-Lock Control adds that commit-boundary control.

Intercompany Elimination has a different purpose: reciprocal consolidation worksheet entries. It does not erase every related FX effect. The current IFRIC published addendum discussing IAS 21 paragraph 45 reinforces that boundary; its IFRS 18 presentation-category discussion is outside this measurement profile.

You now have a reproducible rate-to-target-to-adjustment calculation, exact evidence identities and a proposal whose accounting signs can be inspected before controlled posting.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

Accessed: 25 October 2026. All Cedar positions, receipts and quotes are synthetic, including forward-dated modeled close scenarios. No actual market rate, customer position or completed posting is claimed.

Supports: the exact paragraph and version roles below. Limitations: software inputs and this bounded calculation do not establish complete IFRS compliance, source authenticity or the accounting classification asserted by a host.

Primary sourceEdition / paragraphRole and limit
AASB 121 issuer adoption of IAS 21Compilation 4, 31 December 2024; prepared 5 March 2025; periods beginning 1 January 2025 before 1 January 2027; comparison with IAS21; paragraphs16,21,23(a),26,28–29,32–33,38–45Paragraph verification for monetary items, closing-rate functional measurement and ordinary exchange differences. Australian-specific provisions are not used; this is not an Australian legal-compliance tool.
IFRS Foundation Lack of Exchangeability noticeAugust 2023 amendments, effective annual periods beginning 1 January 2025, early application allowedCurrent amendment check; this initial profile routes nonexchangeability outside instead of estimating a rate.
IFRS Foundation hyperinflationary presentation-currency noticeNovember 2025 amendments, annual periods beginning 1 January 2027; early application permittedCurrent amendment check; whole-statement presentation translation and hyperinflation are excluded.
IFRIC March 2026 Update, April 2026 published addendumClassification of a Foreign Exchange Difference from an Intragroup Monetary Liability(orAsset), fact pattern and IAS21¶45 discussionConfirms the consolidation interaction: reciprocal balance elimination does not erase applicable FX effects. The IFRS18 category discussion is outside our selected measurement profile.

The IFRS Foundation issued-standard PDF links redirected to subscription access; the public first-party AASB adoption supplies paragraph text and identifies its IAS21 incorporation. No gated PDF is claimed as read. The detailed private production research register preserves version/access decisions. No full standard text is reproduced.

Author choices: strict extracted receipt and normalized rate revision schema, fixed EUR/USD exponents2/2, UTC month-end measurement instant, half-even target rounding, resource bounds, account mapping, error codes and output proposal format. Exact arithmetic and sign cases are author-derived; all numerical inputs are synthetic. No source below mandates our JSON API or rounding policy.

Historical example: not useful without licensed quantity/current-carrying snapshots, known-at rate versions and applicable classification/exchangeability evidence. A public filing's aggregate exchange difference does not reproduce a particular position's journal. No calibration, operational performance or financial reporting certification claim.

fx-revaluation.ts
/** Ordinary EUR monetary position -> functional USD proposal. No posting or source authentication. */
type Obj=Record<string,any>;
const REQUEST='schema_version revaluation_id ledger_id entity_id period knowledge_cutoff profile rounding account_map'.split(' ');
const MAP='valuation_account_id fx_gain_account_id fx_loss_account_id'.split(' ');
const BASE='schema_version evidence_id ledger_id entity_id period currency basis knowledge_cutoff observed_at head_sequence receipt_ref'.split(' ');
const POSITION=[...BASE,...'position_id position_version_ref valuation_account_id native_currency foreign_quantity_minor carrying_minor raw_balance_minor nature classification exchangeable hedge_accounting net_investment hyperinflation available_at position_basis_ref'.split(' ')];
const HEAD='schema_version source_id measurement_at observed_at head_revision receipt_ref'.split(' ');
const VERSION='schema_version source_id rate_version_id revision supersedes_revision source_version_ref measurement_at available_at base_currency quote_currency rate'.split(' ');
const ACCOUNT='account_id ledger_id entity_id currency account_type normal_side status'.split(' ');
const NORMAL:Record<string,string>={asset:'debit',expense:'debit',liability:'credit',income:'credit',equity:'credit'};
const INDETERMINATE=new Set(['RATE_SEQUENCE_GAP','INCOMPLETE_RATE_HISTORY','KNOWLEDGE_BEYOND_RATE_SOURCE','MISSING_CLOSING_RATE','POSITION_NOT_AVAILABLE']);
class Invalid extends Error{}
function require(ok:any,code:string):asserts ok{if(!ok)throw new Invalid(code);}
function exact(x:any,keys:string[]):boolean{return x!==null&&typeof x==='object'&&!Array.isArray(x)&&Object.keys(x).length===keys.length&&keys.every(k=>Object.hasOwn(x,k));}
function full(x:any,re:RegExp):boolean{return typeof x==='string'&&re.exec(x)?.[0]===x;}
const identifier=(x:any)=>full(x,/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/);
const unsigned=(x:any,digits:number,positive=false)=>typeof x==='string'&&x.length<=digits&&full(x,positive?/^[1-9][0-9]*$/:/^(0|[1-9][0-9]*)$/);
const signed=(x:any,digits:number)=>typeof x==='string'&&(unsigned(x,digits)||(x.startsWith('-')&&unsigned(x.slice(1),digits,true)));
const period=(x:any)=>full(x,/^[0-9]{4}-(0[1-9]|1[0-2])$/)&&x.slice(0,4)!=='0000';
function stamp(x:any):boolean{if(!full(x,/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/)||x.slice(0,4)==='0000')return false;const d=new Date(x);return !Number.isNaN(d.getTime())&&d.toISOString()===x;}
function closeAt(p:string):string{const y=Number(p.slice(0,4)),m=Number(p.slice(5)),leap=y%4===0&&(y%100!==0||y%400===0);return p+'-'+String([31,leap?29:28,31,30,31,30,31,31,30,31,30,31][m-1])+'T23:59:59.999Z';}
type Rat={n:bigint,d:bigint};
function fraction(n:bigint,d=1n):Rat{let a=n<0n?-n:n,b=d;while(b){const r=a%b;a=b;b=r;}return {n:n/a,d:d/a};}
const wire=(x:Rat)=>({numerator:String(x.n),denominator:String(x.d)});
const canonical=(x:any):any=>Array.isArray(x)?x.map(canonical):x!==null&&typeof x==='object'?Object.fromEntries(Object.keys(x).sort().map(k=>[k,canonical(x[k])])):x;
const equal=(a:any,b:any)=>JSON.stringify(canonical(a))===JSON.stringify(canonical(b));
function failure(code:string):Obj{return {status:INDETERMINATE.has(code)?'indeterminate':'rejected',code,scope:null,position_snapshot:null,selected_rate:null,coverage:null,calculation:null,proposal:null};}
function accountsOk(accounts:any):boolean{
 if(!Array.isArray(accounts)||accounts.length>256)return false;const seen=new Set<string>();
 for(const a of accounts){if(!exact(a,ACCOUNT)||!['account_id','ledger_id','entity_id'].every(k=>identifier(a[k]))||seen.has(a.account_id))return false;seen.add(a.account_id);if(typeof a.currency!=='string'||!['USD','EUR'].includes(a.currency)||typeof a.account_type!=='string'||!Object.hasOwn(NORMAL,a.account_type)||a.normal_side!==NORMAL[a.account_type]||!['active','closed'].includes(a.status))return false;}return true;
}
export function proposeFxRevaluation(request:any,positionSnapshot:any,rateHistory:any,context:any):Obj{
 try{
  require(exact(request,REQUEST),'INVALID_SCHEMA');const q=request;
  require(q.schema_version==='d30-fx-revaluation-v1','UNSUPPORTED_SCHEMA');require(['revaluation_id','ledger_id','entity_id'].every(k=>identifier(q[k])),'INVALID_IDENTIFIER');require(period(q.period),'INVALID_PERIOD');const measurement=closeAt(q.period);
  require(stamp(q.knowledge_cutoff),'INVALID_TIMESTAMP');const K=q.knowledge_cutoff;require(K>=measurement,'KNOWLEDGE_BEFORE_CLOSE');require(q.profile==='ias21-ordinary-eur-usd-v1','UNSUPPORTED_PROFILE');require(q.rounding==='half-even-target-v1','UNSUPPORTED_ROUNDING');require(exact(q.account_map,MAP)&&Object.values(q.account_map).every(identifier)&&new Set(Object.values(q.account_map)).size===3,'INVALID_ACCOUNT_MAP');
  require(exact(context,['accounts','rate_head']),'INVALID_CONTEXT');require(accountsOk(context.accounts),'INVALID_ACCOUNTS');const h=context.rate_head;require(exact(h,HEAD),'INVALID_RATE_HEAD');require(h.schema_version==='d30-fx-rate-head-v1'&&identifier(h.source_id)&&identifier(h.receipt_ref)&&stamp(h.measurement_at)&&stamp(h.observed_at)&&unsigned(h.head_revision,30),'INVALID_RATE_HEAD');require(h.measurement_at===measurement,'RATE_SCOPE_MISMATCH');
  const p=positionSnapshot;require(exact(p,POSITION)&&p.schema_version==='d30-close-source-v1','INVALID_POSITION');require(['evidence_id','ledger_id','entity_id','receipt_ref','position_id','position_version_ref','valuation_account_id','position_basis_ref'].every(k=>identifier(p[k])),'INVALID_IDENTIFIER');require(period(p.period)&&stamp(p.knowledge_cutoff)&&stamp(p.observed_at)&&stamp(p.available_at)&&unsigned(p.head_sequence,30),'INVALID_POSITION');require(['ledger_id','entity_id','period','knowledge_cutoff'].every(k=>p[k]===q[k])&&p.currency==='USD'&&p.basis==='functional-usd','POSITION_SCOPE_MISMATCH');require(p.native_currency==='EUR','UNSUPPORTED_POSITION_CURRENCY');require(unsigned(p.foreign_quantity_minor,30,true)&&unsigned(p.carrying_minor,60),'INVALID_AMOUNT');require(signed(p.raw_balance_minor,60),'INVALID_RAW_BALANCE');require(['asset','liability'].includes(p.nature),'INVALID_POSITION');const carrying=BigInt(p.carrying_minor);require(BigInt(p.raw_balance_minor)===(p.nature==='asset'?carrying:-carrying),'INVALID_RAW_BALANCE');require(['exchangeable','hedge_accounting','net_investment','hyperinflation'].every(k=>typeof p[k]==='boolean'),'INVALID_POSITION');require(p.classification==='monetary','UNSUPPORTED_MONETARY_PROFILE');require(p.exchangeable,'NONEXCHANGEABLE_UNSUPPORTED');require(!['hedge_accounting','net_investment','hyperinflation'].some(k=>p[k]),'UNSUPPORTED_EXCEPTION_PROFILE');require(p.observed_at>=K&&p.available_at>=measurement&&p.available_at<=p.observed_at,'POSITION_CLOCK_VIOLATION');require(p.available_at<=K,'POSITION_NOT_AVAILABLE');require(p.valuation_account_id===q.account_map.valuation_account_id,'SOURCE_ACCOUNT_MISMATCH');
  const amap=new Map<string,Obj>(context.accounts.map((a:Obj)=>[a.account_id,a]));for(const [key,role] of [['valuation_account_id',p.nature],['fx_gain_account_id','income'],['fx_loss_account_id','expense']]){const a=amap.get(q.account_map[key]);require(a!==undefined&&a.ledger_id===q.ledger_id&&a.entity_id===q.entity_id&&a.currency==='USD'&&a.status==='active'&&a.account_type===role,'ACCOUNT_ROLE_MISMATCH');}
  require(Array.isArray(rateHistory)&&rateHistory.length<=512,'INVALID_RATE_HISTORY');const ids=new Map<string,Obj>(),revisions=new Set<string>(),sourceVersions=new Set<string>();
  for(const v of rateHistory){require(exact(v,VERSION)&&v.schema_version==='d30-fx-rate-version-v1','INVALID_RATE_VERSION');require(['source_id','rate_version_id','source_version_ref'].every(k=>identifier(v[k])),'INVALID_IDENTIFIER');require(unsigned(v.revision,30,true)&&(v.supersedes_revision===null||unsigned(v.supersedes_revision,30,true)),'INVALID_RATE_VERSION');require(stamp(v.measurement_at)&&stamp(v.available_at),'INVALID_TIMESTAMP');require(v.source_id===h.source_id&&v.measurement_at===measurement,'RATE_SCOPE_MISMATCH');require((v.base_currency==='EUR'&&v.quote_currency==='USD')||(v.base_currency==='USD'&&v.quote_currency==='EUR'),'UNSUPPORTED_QUOTE_PAIR');const rate=v.rate;require(exact(rate,['numerator','denominator'])&&unsigned(rate.numerator,30,true)&&unsigned(rate.denominator,30,true),'INVALID_RATE');const f=fraction(BigInt(rate.numerator),BigInt(rate.denominator));require(equal(wire(f),rate),'INVALID_RATE');if(ids.has(v.rate_version_id))throw new Invalid(equal(ids.get(v.rate_version_id),v)?'DUPLICATE_RATE_VERSION':'RATE_VERSION_ID_CONFLICT');require(!revisions.has(v.revision),'REVISION_ID_CONFLICT');require(!sourceVersions.has(v.source_version_ref),'DUPLICATE_SOURCE_VERSION');ids.set(v.rate_version_id,v);revisions.add(v.revision);sourceVersions.add(v.source_version_ref);}
  const ordered:Obj[]=[...rateHistory].sort((a,b)=>BigInt(a.revision)<BigInt(b.revision)?-1:1);let lastReceipt=measurement;
  ordered.forEach((v,index)=>{require(BigInt(v.revision)===BigInt(index+1),'RATE_SEQUENCE_GAP');require(v.supersedes_revision===(index===0?null:String(index)),'INVALID_REVISION_CHAIN');require(v.available_at>=lastReceipt&&v.available_at<=h.observed_at,'RATE_CLOCK_VIOLATION');lastReceipt=v.available_at;});const head=BigInt(h.head_revision);require(BigInt(ordered.length)<=head,'HEAD_REVISION_MISMATCH');require(BigInt(ordered.length)===head,'INCOMPLETE_RATE_HISTORY');require(K<=h.observed_at,'KNOWLEDGE_BEYOND_RATE_SOURCE');const visible=ordered.filter(v=>v.available_at<=K);require(visible.length>0,'MISSING_CLOSING_RATE');const selected=visible[visible.length-1]!;
  let r=fraction(BigInt(selected.rate.numerator),BigInt(selected.rate.denominator));if(selected.base_currency==='USD')r=fraction(r.d,r.n);const quantity=BigInt(p.foreign_quantity_minor),exactTarget=fraction(quantity*r.n,r.d),whole=exactTarget.n/exactTarget.d,rem=exactTarget.n%exactTarget.d,target=whole+((2n*rem>exactTarget.d||(2n*rem===exactTarget.d&&whole%2n===1n))?1n:0n),delta=target-carrying,amount=delta<0n?-delta:delta;const gainLoss=delta===0n?'none':(delta>0n)===(p.nature==='asset')?'gain':'loss',status=delta===0n?'zero':amount>10n**30n-1n?'blocked':'ready';let legs:Obj[]=[];
  if(status==='ready'){const valuation=q.account_map.valuation_account_id,gain=q.account_map.fx_gain_account_id,loss=q.account_map.fx_loss_account_id;const [debit,credit]=p.nature==='asset'&&delta>0n?[valuation,gain]:delta>0n||p.nature==='asset'?[loss,valuation]:[valuation,gain];legs=[{account_id:debit,side:'debit',amount_minor:String(amount)},{account_id:credit,side:'credit',amount_minor:String(amount)}];}
  return {status:'ok',code:'FX_REVALUATION_CALCULATED',scope:{revaluation_id:q.revaluation_id,ledger_id:q.ledger_id,entity_id:q.entity_id,period:q.period,currency:'USD',basis:'functional-usd',measurement_at:measurement,knowledge_cutoff:K},position_snapshot:structuredClone(p),selected_rate:structuredClone(selected),coverage:{source_id:h.source_id,receipt_ref:h.receipt_ref,observed_at:h.observed_at,head_revision:h.head_revision,validated_through_revision:String(ordered.length),visible_through_revision:selected.revision},calculation:{foreign_quantity_minor:p.foreign_quantity_minor,carrying_before_minor:p.carrying_minor,normalized_usd_per_eur:wire(r),exact_target_minor:wire(exactTarget),rounded_target_minor:String(target),rounding_residual_minor:wire(fraction(exactTarget.n-target*exactTarget.d,exactTarget.d)),adjustment_minor:String(delta),gain_loss:gainLoss,foreign_quantity_after_minor:p.foreign_quantity_minor},proposal:{status,code:({ready:'POSTING_PROPOSAL',zero:'NO_ADJUSTMENT',blocked:'AMOUNT_LIMIT'} as Obj)[status],currency:'USD',booking_period:q.period,effective_at:measurement,source_available_at:p.available_at>selected.available_at?p.available_at:selected.available_at,amount_minor:String(amount),legs}};
 }catch(error){if(error instanceof Invalid)return failure(error.message);throw error;}
}
Full-height labfx labOpen full screen
Written by

Fintech engineer building market-data and financial systems, and the author of every article, glossary record, and reference implementation on The Fintech Builder.