diff --git a/data/brokers.json b/data/brokers.json index 1fd360d..36bf62e 100644 --- a/data/brokers.json +++ b/data/brokers.json @@ -77,9 +77,15 @@ { "name": "DEGIRO", "logo": "degiro.png", - "product": "Basic - Kernselectie", - "mutualFundTransactionFee": 0, + "product": "Basic", + "mutualFundTransactionFee": { + "flat": 3.9 + }, "etfTransactionFee": { + "flat": 3 + }, + "selectionMutualFundTransactionFee": 0, + "selectionEtfTransactionFee": { "flat": 1 }, "costOverview": "https://www.degiro.nl/data/pdf/Tarievenoverzicht.pdf", @@ -94,27 +100,19 @@ "maximum": 40 }, "serviceFeeFrequency": "monthly", - "mutualFundTransactionFee": 0, + "mutualFundTransactionFee": { + "percentage": 0.15, + "minimum": 8, + "maximum": 150 + }, "etfTransactionFee": { "percentage": 0.08, "minimum": 2, "maximum": 150, "_comment": "Based on the Bronze account, the fee is lower for accounts with higher equity." }, - "costOverview": "https://www.home.saxo/-/media/documents/regional/nl/pricing-unified.pdf?revision=db393ec4-e48a-4be2-a5e4-7751670cd203", - "website": "https://www.home.saxo/nl-nl" - }, - { - "name": "Saxo Bank AutoInvest", - "logo": "saxo.png", - "product": "Zelf Beleggen AutoInvest", - "serviceFee": { - "percentage": 0.01, - "maximum": 40 - }, - "serviceFeeFrequency": "monthly", - "mutualFundTransactionFee": 0, - "etfTransactionFee": 0, + "selectionMutualFundTransactionFee": 0, + "selectionEtfTransactionFee": 0, "costOverview": "https://www.home.saxo/-/media/documents/regional/nl/pricing-unified.pdf?revision=db393ec4-e48a-4be2-a5e4-7751670cd203", "website": "https://www.home.saxo/nl-nl" }, diff --git a/data/fund_selections.json b/data/fund_selections.json new file mode 100644 index 0000000..caa9287 --- /dev/null +++ b/data/fund_selections.json @@ -0,0 +1,265 @@ +[ + { + "broker": "DEGIRO", + "name": "Kernselectie", + "sources": [ + "https://www.degiro.nl/tarieven/etf-kernselectie", + "https://www.degiro.nl/data/pdf/DEGIRO_Beleggingsfondsen_Kernselectie.pdf" + ], + "isins": [ + "IE0008471009", + "IE00B02KXK85", + "IE0005042456", + "IE00B0M62S72", + "IE00B0M62Y33", + "IE00B14X4T88", + "IE00B1FZS350", + "IE00B4K48X80", + "IE00B4L5Y983", + "IE00B4L5YC18", + "NL0009272749", + "FR0007052782", + "IE0032077012", + "LU0252633754", + "DE000A0F5UH1", + "DE000A0H0728", + "FR0010524777", + "IE00B6YX5D40", + "IE00B5M1WJ87", + "NL0009690239", + "IE00B4X9L533", + "IE00B44T3H88", + "NL0010408704", + "IE00B5BMR087", + "IE00B9CQXS71", + "IE00B3RBWM25", + "IE00B3VVMM84", + "IE00B8GKDB10", + "IE00B945VV12", + "IE00B3XXRP09", + "FR0010251744", + "IE00BKM4GZ66", + "IE00BJ38QD84", + "IE00B53SZB19", + "IE00BSKRJZ44", + "IE00BP3QZ825", + "LU0908500753", + "IE00B3WJKG14", + "IE00BZ163M45", + "IE00BM67HT60", + "IE00BYTRRD19", + "NL0011683594", + "IE00B1TXK627", + "IE00B1XNHC34", + "IE00B6R52036", + "IE00B6R51Z18", + "IE00BYZK4552", + "IE00BYZK4776", + "IE00BQQP9F84", + "IE00BQQP9G91", + "IE00BYPLS672", + "IE00BYXG2H39", + "LU1681046931", + "IE00BF0M2Z96", + "IE00BF4RFH31", + "IE00BZCQB185", + "IE00BWBXM948", + "IE00BG0J4C88", + "LU1829218749", + "IE00BGL86Z12", + "IE00BYX2JD69", + "IE00BFXR7892", + "IE00BYWQWR46", + "IE00BK5BQT80", + "IE00BJGWQN72", + "IE00BLRPQH31", + "IE00BMC38736", + "IE00BMYDM794", + "IE000I8KRLL9", + "IE00BLPK3577", + "IE00BMDH1538", + "IE00BMDKNW35", + "IE00BNG8L278", + "IE00BLRB0242", + "IE00B53L4X51", + "NL0006294035", + "NL0006294092", + "LU0523293024", + "LU0837973634", + "LU0837975928", + "LU0837985992", + "LU0837965291", + "LU0837967826", + "LU0837977205", + "LU0837979243", + "LU0482910154", + "LU0293315536", + "LU0972998891", + "LU0348832204", + "LU0839530630", + "LU0823047039", + "LU0496654400", + "LU0252963896", + "LU0827889055", + "LU0252963623", + "LU0252963383", + "LU0376438312", + "LU1005243503", + "LU0823432611", + "IE00B4VRKF23", + "IE00B5MQDC34", + "DE000DWS1VB9", + "DE000DWS1UN6", + "LU0871835996", + "LU0179220412", + "LU0740823785", + "LU0826453069", + "LU0273175025", + "LU0329761406", + "LU0708389589", + "LU0861992385", + "LU0861997004", + "LU0976192475", + "LU0605515880", + "LU0318939252", + "LU0889564604", + "LU0976564798", + "LU0959059279", + "LU0976565506", + "LU0792612466", + "LU0830625769", + "LU0858293193", + "LU0830672258", + "LU0828814094", + "LU0103814108", + "LU0103813043", + "LU0927663491", + "IE00BD616T89", + "IE00B5646799", + "BE6246057333", + "BE6246074502", + "LU0726357444", + "LU0386875149", + "LU0760712090", + "LU0953040879", + "IE0033989843", + "IE00B1JC0H05", + "IE00B2R34Y72", + "IE00B39T3767", + "IE00B62L8426", + "IE0033666466", + "IE00B8DTNZ55", + "IE00B3FNF987", + "IE00B87KCF77", + "LU0620638824", + "LU0792901570", + "LU0177222394", + "LU0966865874", + "LU0969111813", + "LU0562314715", + "LU0201322640", + "GB00B3T70242", + "GB00B3M84Q67", + "GB00B3D8PZ13", + "GB00B1XK5Q40", + "LU0837965457", + "LU0837977544", + "LU0837968121", + "LU0837985646", + "LU0714743050", + "LU0256881474", + "LU0839534970", + "LU0252966055", + "LU0252966485", + "LU0329592371", + "LU0532707519", + "LU0297941469", + "LU0368266812", + "LU0145657366", + "LU0507266574", + "LU0273179522", + "LU0616856778", + "LU0346390270", + "LU0936575868", + "LU0871812789", + "LU0858299471", + "LU0858291221", + "LU0830653209", + "LU0955861553", + "LU0103815170", + "LU0289470972", + "LU0129499017", + "IE00B45R5B91", + "LU0606353232", + "LU0788035094", + "LU0386856941", + "IE00B0C18065", + "IE0032568887", + "IE0002420739", + "IE0032568770", + "IE00B80G9288", + "IE0032379574", + "IE0033591748", + "IE00B3W9BG81", + "IE00B639QZ24", + "IE00B0CNPY59", + "LU0792910563", + "LU0968427160", + "LU0966867227", + "IE00B4ZJ4188", + "LU0145647722", + "FR0010321802", + "FR0010321828", + "IE0002461055", + "IE0002460867", + "LU0995140356", + "NO0008000445", + "NO0008004009", + "NO0010140502", + "LU0212178916", + "LU0171293920", + "LU0876475368", + "LU0983346296", + "LU0860350577", + "LU0860350494", + "LU0885324813", + "LU0860350221", + "LU0860350148", + "LU0860350064", + "LU0929966207", + "LU0828814250", + "LU0892274704", + "LU0892274969", + "LU0976556265", + "LU0995107140", + "LU0828813526", + "LU0942194852", + "LU0892275263", + "LU0212180813", + "LU0823417224", + "LU1165135879", + "LU0252964944", + "LU0329760937", + "LU0992630599", + "LU0827889485", + "LU1829331989", + "IE00BD5HXH43" + ] + }, + { + "broker": "Saxo Bank", + "name": "AutoInvest", + "sources": [ + "https://github.com/nicwortel/indexfondsenvergelijken.nl/issues/51" + ], + "isins": [ + "IE00B3RBWM25", + "IE00BK5BQT80", + "NL0006311771", + "NL0011309349", + "NL0014332587", + "IE00B4L5Y983", + "IE00BKM4GZ66" + ] + } +] diff --git a/data/portfolios.json b/data/portfolios.json index 4c41db8..b20037d 100644 --- a/data/portfolios.json +++ b/data/portfolios.json @@ -48,7 +48,7 @@ "ING", "ABN Amro", "DEGIRO", - "Saxo Bank AutoInvest", + "Saxo Bank", "Interactive Brokers", "Lynx", "Easybroker" @@ -65,7 +65,7 @@ "DEGIRO", "Interactive Brokers", "Lynx", - "Saxo Bank AutoInvest", + "Saxo Bank", "Easybroker" ] }, @@ -92,7 +92,7 @@ } ], "brokers": [ - "Saxo Bank AutoInvest", + "Saxo Bank", "Interactive Brokers", "Lynx", "Easybroker", @@ -112,7 +112,7 @@ } ], "brokers": [ - "Saxo Bank AutoInvest", + "Saxo Bank", "Interactive Brokers", "Lynx", "Easybroker" diff --git a/src/Broker.ts b/src/Broker.ts index 978f8c4..744a865 100644 --- a/src/Broker.ts +++ b/src/Broker.ts @@ -5,6 +5,7 @@ import {MutualFund} from "./Fund/MutualFund"; import {DeductibleFee} from "./Pricing/DeductibleFee"; import {Fee} from "./Pricing/Fee"; import {Transaction} from "./Transaction"; +import {FundSelection} from "./FundSelection"; export class Broker { constructor( @@ -15,6 +16,9 @@ export class Broker { public serviceFeeCalculation: 'averageEndOfMonth' | 'averageOfQuarter' | 'endOfQuarter', public mutualFundTransactionFee: Fee, public etfTransactionFee: Fee, + public selectionMutualFundTransactionFee: Fee, + public selectionEtfTransactionFee: Fee, + public fundSelections: FundSelection[], public dividendDistributionFee: Fee, public costOverview: string, public logo?: string, @@ -63,6 +67,16 @@ export class Broker { } private getTransactionFeeFor(fund: Fund): Fee { + if (this.fundSelections.some((selection) => selection.contains(fund))) { + if (fund instanceof MutualFund) { + return this.selectionMutualFundTransactionFee; + } + + if (fund instanceof Etf) { + return this.selectionEtfTransactionFee; + } + } + if (fund instanceof MutualFund) { return this.mutualFundTransactionFee; } diff --git a/src/BrokerRepository.ts b/src/BrokerRepository.ts index 8878eaa..899a0ae 100644 --- a/src/BrokerRepository.ts +++ b/src/BrokerRepository.ts @@ -1,6 +1,8 @@ import {Broker} from "./Broker"; import brokers from "../data/brokers.json"; +import fundSelectionsData from "../data/fund_selections.json"; import {FeeFactory} from "./Pricing/FeeFactory"; +import {FundSelection} from "./FundSelection"; export class BrokerRepository { public getAll(): Array { @@ -10,12 +12,18 @@ export class BrokerRepository { const serviceFee = feeFactory.create(data.serviceFee); const mutualFundTransactionFee = feeFactory.create(data.mutualFundTransactionFee); const etfTransactionFee = feeFactory.create(data.etfTransactionFee); + const selectionMutualFundTransactionFee = feeFactory.create(data.selectionMutualFundTransactionFee); + const selectionEtfTransactionFee = feeFactory.create(data.selectionEtfTransactionFee); const dividendDistributionFee = feeFactory.create(data.dividendDistributionFee); if (data.logo) { require('../assets/images/' + data.logo); } + const fundSelections: FundSelection[] = fundSelectionsData.filter((fundSelection: any) => fundSelection.broker === data.name).map((fundSelection: any) => { + return new FundSelection(fundSelection.name, fundSelection.isins); + }); + return new Broker( data.name, data.product, @@ -24,6 +32,9 @@ export class BrokerRepository { data.serviceFeeCalculation, mutualFundTransactionFee, etfTransactionFee, + selectionMutualFundTransactionFee, + selectionEtfTransactionFee, + fundSelections, dividendDistributionFee, data.costOverview, data.logo ? data.logo : null, diff --git a/src/FundSelection.ts b/src/FundSelection.ts new file mode 100644 index 0000000..5806aed --- /dev/null +++ b/src/FundSelection.ts @@ -0,0 +1,13 @@ +import {Fund} from "./Fund/Fund"; + +export class FundSelection { + constructor( + public name: string, + public isins: string[] + ) { + } + + public contains(fund: Fund): boolean { + return this.isins.includes(fund.getIsin()); + } +} diff --git a/src/Pricing/FeeFactory.ts b/src/Pricing/FeeFactory.ts index e0f75f2..dfdb52a 100644 --- a/src/Pricing/FeeFactory.ts +++ b/src/Pricing/FeeFactory.ts @@ -44,7 +44,7 @@ export class FeeFactory { fee = new VolumeFee(volumes); } else if (typeof data.flat == 'number') { - fee = new FlatFee(new Money(data.flat, this.currency)); + fee = new FlatFee(new Money(data.flat.toString(), this.currency)); } else if (typeof data.percentage == 'number') { fee = new PercentageFee(data.percentage); } else { diff --git a/templates/card.html.twig b/templates/card.html.twig index d1b5a32..0cacb86 100644 --- a/templates/card.html.twig +++ b/templates/card.html.twig @@ -150,6 +150,15 @@ ETF's: {{ broker.etfTransactionFee.describe }} + {% for fundSelection in broker.fundSelections %} + + Transactiekosten {{ fundSelection.name }} + + Indexfondsen: {{ broker.selectionMutualFundTransactionFee.describe }}
+ ETF's: {{ broker.selectionEtfTransactionFee.describe }} + + + {% endfor %} {% if broker.dividendDistributionFee.describe != 'geen' %} Kosten dividenduitkering diff --git a/tests/Broker.test.ts b/tests/Broker.test.ts index d96999c..a533bc6 100644 --- a/tests/Broker.test.ts +++ b/tests/Broker.test.ts @@ -4,6 +4,9 @@ import {PercentageFee} from "../src/Pricing/PercentageFee"; import {Tier, TieredFee} from "../src/Pricing/TieredFee"; import {Transaction} from "../src/Transaction"; import {FundFactory} from "./FundFactory"; +import {NullFee} from "../src/Pricing/NullFee"; +import {FundSelection} from "../src/FundSelection"; +import {FlatFee} from "../src/Pricing/FlatFee"; const fundFactory = new FundFactory(); @@ -19,6 +22,9 @@ test('Calculates the cost of a transaction', () => { 'averageEndOfMonth', new PercentageFee(1), new PercentageFee(1), + new NullFee(), + new NullFee(), + [], new PercentageFee(0), '' ); @@ -27,3 +33,30 @@ test('Calculates the cost of a transaction', () => { expect(transactionCosts).toEqual(new Money(1, 'EUR')); }); + +test('Calculates the transaction cost of a fund from a fund selection', () => { + const baseFee = new Money(0, 'EUR'); + const serviceFee = new TieredFee([new Tier(null, new PercentageFee(0))]); + + const broker = new Broker( + 'TestBroker', + 'Product', + serviceFee, + 'quarterly', + 'averageEndOfMonth', + new FlatFee(new Money(10, 'EUR')), + new FlatFee(new Money(10, 'EUR')), + new FlatFee(new Money(1, 'EUR')), + new FlatFee(new Money(1, 'EUR')), + [ + new FundSelection('Kernselectie', ['NL0011223333', 'NL0011223334']) + ], + new PercentageFee(0), + '' + ); + + const fund = fundFactory.createMutualFund(0, 0, 0, 0, 'NL0011223333'); + const transactionCosts = broker.getTransactionCosts(new Transaction(fund, new Money(100, 'EUR'))); + + expect(transactionCosts).toEqual(new Money(1, 'EUR')); +}); diff --git a/tests/FundFactory.ts b/tests/FundFactory.ts index d942145..01fc832 100644 --- a/tests/FundFactory.ts +++ b/tests/FundFactory.ts @@ -7,14 +7,15 @@ export class FundFactory { totalExpenseRatio: number = 0.1, dividendLeak: number = 0.2, esgExclusionsPercentage: number = 0, - indexMarketCapPercentage: number = 80 + indexMarketCapPercentage: number = 80, + isin: string = 'ISIN' ): MutualFund { const index = this.createIndex(indexMarketCapPercentage); return new MutualFund( 'Name', 'SYM', - 'ISIN', + isin, '', new Percentage(totalExpenseRatio), new Percentage(0), diff --git a/tests/Simulation.test.ts b/tests/Simulation.test.ts index aa08474..1ecc896 100644 --- a/tests/Simulation.test.ts +++ b/tests/Simulation.test.ts @@ -6,6 +6,7 @@ import {PercentageFee} from "../src/Pricing/PercentageFee"; import {Tier, TieredFee} from "../src/Pricing/TieredFee"; import {Simulation} from "../src/Simulation"; import {FundFactory} from "./FundFactory"; +import {NullFee} from "../src/Pricing/NullFee"; const fundFactory = new FundFactory(); @@ -21,6 +22,9 @@ function createSimulation(initialInvestment: number, monthlyInvestment: number, 'endOfQuarter', new PercentageFee(0), new PercentageFee(0), + new NullFee(), + new NullFee(), + [], new PercentageFee(0), '' ),