123 lines
3.9 KiB
Dart
123 lines
3.9 KiB
Dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
import '../../../common/domain/metal_type.dart';
|
|
import '../../market/data/mock_market_repository.dart';
|
|
import '../data/asset_models.dart';
|
|
import '../data/mock_asset_repository.dart';
|
|
|
|
final assetPortfolioControllerProvider =
|
|
StateNotifierProvider<AssetPortfolioController, PortfolioSummary>((ref) {
|
|
final holdings = ref.watch(mockAssetRepositoryProvider).loadHoldings();
|
|
final market = ref.watch(mockMarketRepositoryProvider);
|
|
return AssetPortfolioController(
|
|
holdings: holdings,
|
|
spotPriceFor: (metal) => market.quoteFor(metal).spotPrice,
|
|
changeFor: (metal) => market.quoteFor(metal).changeAmount,
|
|
);
|
|
});
|
|
|
|
class AssetPortfolioController extends StateNotifier<PortfolioSummary> {
|
|
AssetPortfolioController({
|
|
required List<GoldAssetHolding> holdings,
|
|
required double Function(MetalType metal) spotPriceFor,
|
|
required double Function(MetalType metal) changeFor,
|
|
this.recycleDiscount = 0.97,
|
|
}) : _holdings = holdings,
|
|
_spotPriceFor = spotPriceFor,
|
|
_changeFor = changeFor,
|
|
super(
|
|
_buildSummary(
|
|
holdings: holdings,
|
|
spotPriceFor: spotPriceFor,
|
|
changeFor: changeFor,
|
|
recycleDiscount: recycleDiscount,
|
|
),
|
|
);
|
|
|
|
final List<GoldAssetHolding> _holdings;
|
|
final double Function(MetalType metal) _spotPriceFor;
|
|
final double Function(MetalType metal) _changeFor;
|
|
final double recycleDiscount;
|
|
|
|
void addHolding(GoldAssetHolding holding) {
|
|
_holdings.add(holding);
|
|
state = _buildSummary(
|
|
holdings: _holdings,
|
|
spotPriceFor: _spotPriceFor,
|
|
changeFor: _changeFor,
|
|
recycleDiscount: recycleDiscount,
|
|
);
|
|
}
|
|
|
|
static PortfolioSummary _buildSummary({
|
|
required List<GoldAssetHolding> holdings,
|
|
required double Function(MetalType metal) spotPriceFor,
|
|
required double Function(MetalType metal) changeFor,
|
|
required double recycleDiscount,
|
|
}) {
|
|
final active = holdings
|
|
.where((holding) => holding.status == HoldingStatus.active)
|
|
.toList(growable: false);
|
|
final valuations = [
|
|
for (final holding in active)
|
|
HoldingValuation(
|
|
holding: holding,
|
|
materialValue: holding.materialValue(spotPriceFor(holding.metal)),
|
|
todayChange:
|
|
holding.weightGram * holding.purity * changeFor(holding.metal),
|
|
),
|
|
];
|
|
|
|
final totalValue = valuations.fold<double>(
|
|
0,
|
|
(sum, item) => sum + item.materialValue,
|
|
);
|
|
final todayChange = valuations.fold<double>(
|
|
0,
|
|
(sum, item) => sum + item.todayChange,
|
|
);
|
|
final totalCost = active.fold<double>(
|
|
0,
|
|
(sum, holding) => sum + (holding.costAmount ?? 0),
|
|
);
|
|
final totalWeight = active.fold<double>(
|
|
0,
|
|
(sum, holding) => sum + holding.weightGram,
|
|
);
|
|
|
|
return PortfolioSummary(
|
|
totalValue: totalValue,
|
|
todayChangeAmount: todayChange,
|
|
todayChangePercent: totalValue == 0 ? 0 : todayChange / totalValue * 100,
|
|
totalCost: totalCost,
|
|
totalWeightGram: totalWeight,
|
|
recycleReferenceValue: totalValue * recycleDiscount,
|
|
breakdowns: [
|
|
for (final metal in MetalType.values)
|
|
_breakdownFor(metal, active, spotPriceFor),
|
|
].where((item) => item.count > 0).toList(growable: false),
|
|
holdings: valuations,
|
|
);
|
|
}
|
|
|
|
static MetalBreakdown _breakdownFor(
|
|
MetalType metal,
|
|
List<GoldAssetHolding> holdings,
|
|
double Function(MetalType metal) spotPriceFor,
|
|
) {
|
|
final items = holdings.where((holding) => holding.metal == metal).toList();
|
|
return MetalBreakdown(
|
|
metal: metal,
|
|
value: items.fold<double>(
|
|
0,
|
|
(sum, holding) => sum + holding.materialValue(spotPriceFor(metal)),
|
|
),
|
|
weightGram: items.fold<double>(
|
|
0,
|
|
(sum, holding) => sum + holding.weightGram,
|
|
),
|
|
count: items.length,
|
|
);
|
|
}
|
|
}
|