decred.org/dcrdex@v1.0.5/client/webserver/site/src/js/account.ts (about) 1 import Doc from './doc' 2 import { 3 OrderTypeLimit, 4 OrderTypeMarket, 5 OrderTypeCancel, 6 StatusEpoch, 7 StatusBooked, 8 RateEncodingFactor, 9 MatchSideMaker, 10 MakerRedeemed, 11 TakerSwapCast, 12 ImmediateTiF 13 } from './orderutil' 14 import { 15 app, 16 PageElement, 17 ExchangeAuth, 18 Order, 19 Market 20 } from './registry' 21 22 export const bondReserveMultiplier = 2 // Reserves for next bond 23 export const perTierBaseParcelLimit = 2 24 export const parcelLimitScoreMultiplier = 3 25 26 export class ReputationMeter { 27 page: Record<string, PageElement> 28 host: string 29 30 constructor (div: PageElement) { 31 this.page = Doc.parseTemplate(div) 32 Doc.cleanTemplates(this.page.penaltyMarkerTmpl) 33 } 34 35 setHost (host: string) { 36 this.host = host 37 } 38 39 update () { 40 const { page, host } = this 41 const { auth, maxScore, penaltyThreshold } = app().exchanges[host] 42 const { rep: { score } } = auth 43 44 const displayTier = strongTier(auth) 45 46 const minScore = displayTier ? displayTier * penaltyThreshold * -1 : penaltyThreshold * -1 // Just for looks 47 const warnPct = 25 48 const scorePct = 100 - warnPct 49 page.scoreWarn.style.width = `${warnPct}%` 50 const pos = score >= 0 ? warnPct + (score / maxScore) * scorePct : warnPct - (Math.min(warnPct, score / minScore * warnPct)) 51 52 page.scorePointer.style.left = `${pos}%` 53 page.scoreMin.textContent = String(minScore) 54 page.scoreMax.textContent = String(maxScore) 55 const bonus = limitBonus(score, maxScore) 56 page.limitBonus.textContent = bonus.toFixed(1) 57 for (const m of Doc.applySelector(page.scoreTray, '.penalty-marker')) m.remove() 58 if (displayTier > 1) { 59 const markerPct = warnPct / displayTier 60 for (let i = 1; i < displayTier; i++) { 61 const div = page.penaltyMarkerTmpl.cloneNode(true) as PageElement 62 page.scoreTray.appendChild(div) 63 div.style.left = `${markerPct * i}%` 64 } 65 } 66 page.score.textContent = String(score) 67 page.scoreData.classList.remove('negative', 'positive') 68 if (score > 0) page.scoreData.classList.add('positive') 69 else page.scoreData.classList.add('negative') 70 } 71 } 72 73 /* 74 * strongTier is the effective tier, with some respect for bond overlap, such 75 * that we don't count weak bonds that have already had their replacements 76 * confirmed. 77 */ 78 export function strongTier (auth: ExchangeAuth): number { 79 const { weakStrength, targetTier, effectiveTier } = auth 80 if (effectiveTier > targetTier) { 81 const diff = effectiveTier - targetTier 82 if (weakStrength >= diff) return targetTier 83 return targetTier + (diff - weakStrength) 84 } 85 return effectiveTier 86 } 87 88 export function likelyTaker (ord: Order, rate: number): boolean { 89 if (ord.type === OrderTypeMarket || ord.tif === ImmediateTiF) return true 90 // Must cross the spread to be a taker (not so conservative). 91 if (rate === 0) return false 92 if (ord.sell) return ord.rate < rate 93 return ord.rate > rate 94 } 95 96 const preparcelQuantity = (ord: Order, mkt?: Market, midGap?: number) => { 97 const qty = ord.qty - ord.filled 98 if (ord.type === OrderTypeLimit) return qty 99 if (ord.sell) return qty * ord.rate / RateEncodingFactor 100 const rate = midGap || mkt?.spot?.rate || 0 101 // Caller should not call this for market orders without a mkt arg. 102 if (!mkt) return 0 103 // This is tricky. The server will use the mid-gap rate to convert the 104 // order qty. We don't have a mid-gap rate, only a spot rate. 105 if (rate && (mkt?.spot?.bookVolume || 0) > 0) return qty * RateEncodingFactor / rate 106 return mkt.lotsize // server uses same fallback if book is empty 107 } 108 109 export function epochWeight (ord: Order, mkt: Market, midGap?: number) { 110 if (ord.status !== StatusEpoch) return 0 111 const qty = preparcelQuantity(ord, mkt, midGap) 112 const rate = midGap || mkt.spot?.rate || 0 113 if (likelyTaker(ord, rate)) return qty * 2 114 return qty 115 } 116 117 function bookWeight (ord: Order) { 118 if (ord.status !== StatusBooked) return 0 119 return preparcelQuantity(ord) 120 } 121 122 function settlingWeight (ord: Order) { 123 let sum = 0 124 for (const m of (ord.matches || [])) { 125 if (m.side === MatchSideMaker) { 126 if (m.status > MakerRedeemed) continue 127 } else if (m.status > TakerSwapCast) continue 128 sum += m.qty 129 } 130 return sum 131 } 132 133 function parcelWeight (ord: Order, mkt: Market, midGap?: number) { 134 if (ord.type === OrderTypeCancel) return 0 135 return epochWeight(ord, mkt, midGap) + bookWeight(ord) + settlingWeight(ord) 136 } 137 138 // function roundParcels (p: number): number { 139 // return Math.floor(Math.round((p * 1e8)) / 1e8) 140 // } 141 142 function limitBonus (score: number, maxScore: number): number { 143 return score > 0 ? 1 + score / maxScore * (parcelLimitScoreMultiplier - 1) : 1 144 } 145 146 export function tradingLimits (host: string): [number, number] { // [usedParcels, parcelLimit] 147 const { auth, maxScore, markets } = app().exchanges[host] 148 const { rep: { score } } = auth 149 const tier = strongTier(auth) 150 151 let usedParcels = 0 152 for (const mkt of Object.values(markets)) { 153 let mktWeight = 0 154 for (const ord of (mkt.inflight || [])) mktWeight += parcelWeight(ord, mkt) 155 for (const ord of (mkt.orders || [])) mktWeight += parcelWeight(ord, mkt) 156 usedParcels += (mktWeight / (mkt.parcelsize * mkt.lotsize)) 157 } 158 const parcelLimit = perTierBaseParcelLimit * limitBonus(score, maxScore) * tier 159 return [usedParcels, parcelLimit] 160 }