Pricing a 4y9m 3m forward 3M CDOR swap

The swap conventions for 3M CDOR are SemiAnnual payment with Quarterly reset. If I attempt to price a 4y9m swap starting 3m forward I either end up with the fixed and float payment dates misaligned such that the first fix payment date is after 3m and the first float payment date after 6m, or if I attempt to fix the lastRegularEndDate on the float leg accrual / payment schedule I get an error:

com.opengamma.strata.basics.schedule.ScheduleException: Unable to merge schedule, firstRegularStartDate 2020-01-08 and lastRegularEndDate 2024-10-08 cannot be used to create regular periods of frequency 'P6M'

To give explicit details:
start date = 2020-01-08
end date = 2024-10-08

Gives me the following float leg:

0 = {RatePaymentPeriod@8470} "RatePaymentPeriod{paymentDate=2020-07-08, accrualPeriods=[RateAccrualPeriod{startDate=2020-01-08, endDate=2020-04-08, unadjustedStartDate=2020-01-08, unadjustedEndDate=2020-04-08, yearFraction=0.2493150684931507, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2020-01-08, effectiveDate=2020-01-08, maturityDate=2020-04-08, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2020-04-08, endDate=2020-07-08, unadjustedStartDate=2020-04-08, unadjustedEndDate=2020-07-08, yearFraction=0.2493150684931507, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2020-04-08, effectiveDate=2020-04-08, maturityDate=2020-07-08, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
1 = {RatePaymentPeriod@8471} "RatePaymentPeriod{paymentDate=2021-01-08, accrualPeriods=[RateAccrualPeriod{startDate=2020-07-08, endDate=2020-10-08, unadjustedStartDate=2020-07-08, unadjustedEndDate=2020-10-08, yearFraction=0.25205479452054796, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2020-07-08, effectiveDate=2020-07-08, maturityDate=2020-10-08, yearFraction=0.25205479452054796}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2020-10-08, endDate=2021-01-08, unadjustedStartDate=2020-10-08, unadjustedEndDate=2021-01-08, yearFraction=0.25205479452054796, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2020-10-08, effectiveDate=2020-10-08, maturityDate=2021-01-08, yearFraction=0.25205479452054796}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
2 = {RatePaymentPeriod@8472} "RatePaymentPeriod{paymentDate=2021-07-08, accrualPeriods=[RateAccrualPeriod{startDate=2021-01-08, endDate=2021-04-08, unadjustedStartDate=2021-01-08, unadjustedEndDate=2021-04-08, yearFraction=0.2465753424657534, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2021-01-08, effectiveDate=2021-01-08, maturityDate=2021-04-08, yearFraction=0.2465753424657534}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2021-04-08, endDate=2021-07-08, unadjustedStartDate=2021-04-08, unadjustedEndDate=2021-07-08, yearFraction=0.2493150684931507, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2021-04-08, effectiveDate=2021-04-08, maturityDate=2021-07-08, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
3 = {RatePaymentPeriod@8473} "RatePaymentPeriod{paymentDate=2022-01-10, accrualPeriods=[RateAccrualPeriod{startDate=2021-07-08, endDate=2021-10-08, unadjustedStartDate=2021-07-08, unadjustedEndDate=2021-10-08, yearFraction=0.25205479452054796, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2021-07-08, effectiveDate=2021-07-08, maturityDate=2021-10-08, yearFraction=0.25205479452054796}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2021-10-08, endDate=2022-01-10, unadjustedStartDate=2021-10-08, unadjustedEndDate=2022-01-08, yearFraction=0.25753424657534246, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2021-10-08, effectiveDate=2021-10-08, maturityDate=2022-01-10, yearFraction=0.25753424657534246}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
4 = {RatePaymentPeriod@8474} "RatePaymentPeriod{paymentDate=2022-07-08, accrualPeriods=[RateAccrualPeriod{startDate=2022-01-10, endDate=2022-04-08, unadjustedStartDate=2022-01-08, unadjustedEndDate=2022-04-08, yearFraction=0.2410958904109589, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2022-01-10, effectiveDate=2022-01-10, maturityDate=2022-04-11, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2022-04-08, endDate=2022-07-08, unadjustedStartDate=2022-04-08, unadjustedEndDate=2022-07-08, yearFraction=0.2493150684931507, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2022-04-08, effectiveDate=2022-04-08, maturityDate=2022-07-08, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
5 = {RatePaymentPeriod@8475} "RatePaymentPeriod{paymentDate=2023-01-09, accrualPeriods=[RateAccrualPeriod{startDate=2022-07-08, endDate=2022-10-11, unadjustedStartDate=2022-07-08, unadjustedEndDate=2022-10-08, yearFraction=0.2602739726027397, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2022-07-08, effectiveDate=2022-07-08, maturityDate=2022-10-11, yearFraction=0.2602739726027397}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2022-10-11, endDate=2023-01-09, unadjustedStartDate=2022-10-08, unadjustedEndDate=2023-01-08, yearFraction=0.2465753424657534, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2022-10-11, effectiveDate=2022-10-11, maturityDate=2023-01-11, yearFraction=0.25205479452054796}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
6 = {RatePaymentPeriod@8476} "RatePaymentPeriod{paymentDate=2023-07-10, accrualPeriods=[RateAccrualPeriod{startDate=2023-01-09, endDate=2023-04-10, unadjustedStartDate=2023-01-08, unadjustedEndDate=2023-04-08, yearFraction=0.2493150684931507, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2023-01-09, effectiveDate=2023-01-09, maturityDate=2023-04-10, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2023-04-10, endDate=2023-07-10, unadjustedStartDate=2023-04-08, unadjustedEndDate=2023-07-08, yearFraction=0.2493150684931507, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2023-04-10, effectiveDate=2023-04-10, maturityDate=2023-07-10, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
7 = {RatePaymentPeriod@8477} "RatePaymentPeriod{paymentDate=2024-01-08, accrualPeriods=[RateAccrualPeriod{startDate=2023-07-10, endDate=2023-10-10, unadjustedStartDate=2023-07-08, unadjustedEndDate=2023-10-08, yearFraction=0.25205479452054796, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2023-07-10, effectiveDate=2023-07-10, maturityDate=2023-10-10, yearFraction=0.25205479452054796}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2023-10-10, endDate=2024-01-08, unadjustedStartDate=2023-10-08, unadjustedEndDate=2024-01-08, yearFraction=0.2465753424657534, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2023-10-10, effectiveDate=2023-10-10, maturityDate=2024-01-10, yearFraction=0.25205479452054796}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
8 = {RatePaymentPeriod@8478} "RatePaymentPeriod{paymentDate=2024-07-08, accrualPeriods=[RateAccrualPeriod{startDate=2024-01-08, endDate=2024-04-08, unadjustedStartDate=2024-01-08, unadjustedEndDate=2024-04-08, yearFraction=0.2493150684931507, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2024-01-08, effectiveDate=2024-01-08, maturityDate=2024-04-08, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}, RateAccrualPeriod{startDate=2024-04-08, endDate=2024-07-08, unadjustedStartDate=2024-04-08, unadjustedEndDate=2024-07-08, yearFraction=0.2493150684931507, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2024-04-08, effectiveDate=2024-04-08, maturityDate=2024-07-08, yearFraction=0.2493150684931507}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"
9 = {RatePaymentPeriod@8479} "RatePaymentPeriod{paymentDate=2024-10-08, accrualPeriods=[RateAccrualPeriod{startDate=2024-07-08, endDate=2024-10-08, unadjustedStartDate=2024-07-08, unadjustedEndDate=2024-10-08, yearFraction=0.25205479452054796, rateComputation=IborRateComputation{observation=IborIndexObservation{index=CAD-CDOR-3M, fixingDate=2024-07-08, effectiveDate=2024-07-08, maturityDate=2024-10-08, yearFraction=0.25205479452054796}}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=-1000000.0, compoundingMethod=None}"

And fixed leg:

0 = {RatePaymentPeriod@8491} "RatePaymentPeriod{paymentDate=2020-04-08, accrualPeriods=[RateAccrualPeriod{startDate=2020-01-08, endDate=2020-04-08, unadjustedStartDate=2020-01-08, unadjustedEndDate=2020-04-08, yearFraction=0.2493150684931507, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
1 = {RatePaymentPeriod@8492} "RatePaymentPeriod{paymentDate=2020-10-08, accrualPeriods=[RateAccrualPeriod{startDate=2020-04-08, endDate=2020-10-08, unadjustedStartDate=2020-04-08, unadjustedEndDate=2020-10-08, yearFraction=0.5013698630136987, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
2 = {RatePaymentPeriod@8493} "RatePaymentPeriod{paymentDate=2021-04-08, accrualPeriods=[RateAccrualPeriod{startDate=2020-10-08, endDate=2021-04-08, unadjustedStartDate=2020-10-08, unadjustedEndDate=2021-04-08, yearFraction=0.4986301369863014, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
3 = {RatePaymentPeriod@8494} "RatePaymentPeriod{paymentDate=2021-10-08, accrualPeriods=[RateAccrualPeriod{startDate=2021-04-08, endDate=2021-10-08, unadjustedStartDate=2021-04-08, unadjustedEndDate=2021-10-08, yearFraction=0.5013698630136987, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
4 = {RatePaymentPeriod@8495} "RatePaymentPeriod{paymentDate=2022-04-08, accrualPeriods=[RateAccrualPeriod{startDate=2021-10-08, endDate=2022-04-08, unadjustedStartDate=2021-10-08, unadjustedEndDate=2022-04-08, yearFraction=0.4986301369863014, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
5 = {RatePaymentPeriod@8496} "RatePaymentPeriod{paymentDate=2022-10-11, accrualPeriods=[RateAccrualPeriod{startDate=2022-04-08, endDate=2022-10-11, unadjustedStartDate=2022-04-08, unadjustedEndDate=2022-10-08, yearFraction=0.5095890410958904, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
6 = {RatePaymentPeriod@8497} "RatePaymentPeriod{paymentDate=2023-04-10, accrualPeriods=[RateAccrualPeriod{startDate=2022-10-11, endDate=2023-04-10, unadjustedStartDate=2022-10-08, unadjustedEndDate=2023-04-08, yearFraction=0.4958904109589041, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
7 = {RatePaymentPeriod@8498} "RatePaymentPeriod{paymentDate=2023-10-10, accrualPeriods=[RateAccrualPeriod{startDate=2023-04-10, endDate=2023-10-10, unadjustedStartDate=2023-04-08, unadjustedEndDate=2023-10-08, yearFraction=0.5013698630136987, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
8 = {RatePaymentPeriod@8499} "RatePaymentPeriod{paymentDate=2024-04-08, accrualPeriods=[RateAccrualPeriod{startDate=2023-10-10, endDate=2024-04-08, unadjustedStartDate=2023-10-08, unadjustedEndDate=2024-04-08, yearFraction=0.4958904109589041, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"
9 = {RatePaymentPeriod@8500} "RatePaymentPeriod{paymentDate=2024-10-08, accrualPeriods=[RateAccrualPeriod{startDate=2024-04-08, endDate=2024-10-08, unadjustedStartDate=2024-04-08, unadjustedEndDate=2024-10-08, yearFraction=0.5013698630136987, rateComputation=FixedRateComputation{rate=0.01603431106845703}, gearing=1.0, spread=0.0, negativeRateMethod=AllowNegative}], dayCount=Act/365F, currency=CAD, fxReset=null, notional=1000000.0, compoundingMethod=None}"

I would expect the period dates for both legs to match the fixed leg, e.g. payment in April and October. This is what you’ll see on Bloomberg.

Can you please advise how to construct such a swap?

Providing the same PeriodicSchedule and PaymentSchedule are used on both legs, you will get the same schedule of dates.

To get the payment schedule to start at the beginning of the second accrual schedule you need to set the firstRegularStartDate on the PaymentSchedule:

    ReferenceData refData = ReferenceData.standard();
    BusinessDayAdjustment bda = BusinessDayAdjustment.of(MODIFIED_FOLLOWING, CATO);
    PeriodicSchedule accrualSchedule = PeriodicSchedule.builder()
        .startDate(date(2020, 1, 8))
        .endDate(date(2024, 10, 8))
        .frequency(Frequency.P3M)
        .businessDayAdjustment(bda)
        .build();
    PaymentSchedule paymentSchedule = PaymentSchedule.builder()
        .businessDayAdjustment(bda)
        .paymentFrequency(Frequency.P6M)
        .firstRegularStartDate(date(2020, 4, 8))
        .paymentDateOffset(DaysAdjustment.NONE)
        .build();
    NotionalSchedule notional = NotionalSchedule.of(Currency.CAD, 1_000_000);
    RateCalculationSwapLeg leg = RateCalculationSwapLeg.builder()
        .payReceive(PayReceive.PAY)
        .accrualSchedule(accrualSchedule)
        .paymentSchedule(paymentSchedule)
        .notionalSchedule(notional)
        .calculation(FixedRateCalculation.of(0.01, DayCounts.ACT_365F))
        .build();
    Swap swap = Swap.of(leg);
    ResolvedSwap resolved = swap.resolve(refData);
    System.out.println(JodaBeanSer.PRETTY.jsonWriter().write(resolved));

HTH

Thanks, this is helpful, not sure if it’s feasible to modify the default behavior such that the code I’ve had to write is unnecessary - I am not aware of any counter example that would expect the payments to not align.

I’m happy to share the code block in question on email.

Strata’s model is based a fair bit on FpML which allows the fixed and floating leg payment dates to follow different schedules. ie. its up to the caller to ensure they don’t differ.