feat(swap): 实现互换交易价格精度控制功能 init
- 添加 swapPricePrecisionHelper.js 工具类处理价格精度格式化 - 新增 swappriceprecision.js 配置文件定义各类金融产品的精度规则 - 在 EodPositionRisks.cshtml 和 SwapIncome.cshtml 中引入新的价格格式化脚本 - 替换原有的价格格式化函数为基于产品类型的动态精度控制 - 移除旧的价格验证和标准化逻辑,改用新的精度控制机制 - 添加 vue-swap-price-input 组件用于精确的价格输入控制 - 更新 Controller 中的价格处理逻辑以支持新精度格式化方式
This commit is contained in:
@@ -37,7 +37,6 @@ namespace YLErp.Modules.SwapModule
|
||||
protected virtual long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
|
||||
{
|
||||
NormalizeNotionalValues(unwindData);
|
||||
NormalizeDeliveryPrices(unwindData.FlowEvents);
|
||||
return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve);
|
||||
}
|
||||
|
||||
@@ -48,49 +47,6 @@ namespace YLErp.Modules.SwapModule
|
||||
unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static int GetStorageDeliveryPriceRound(swap_flow_event flowEvent)
|
||||
{
|
||||
if (ConsGlobal.InstrumentType.IsBond(flowEvent?.UnderlyingInstrumentType))
|
||||
{
|
||||
return ConsGlobal.PriceRound;
|
||||
}
|
||||
if (string.IsNullOrEmpty(flowEvent?.UnderlyingCode))
|
||||
{
|
||||
return ConsGlobal.SwapDeliveryPriceRound;
|
||||
}
|
||||
var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(flowEvent?.UnderlyingCode);
|
||||
return underlying?.IsBond() == true ? ConsGlobal.PriceRound : ConsGlobal.SwapDeliveryPriceRound;
|
||||
}
|
||||
|
||||
private static void ValidateDeliveryPrices(UnwindData unwindData)
|
||||
{
|
||||
if (unwindData.FlowEvents == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in unwindData.FlowEvents.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
var roundedPrice = Math.Round(item.TradingAmountAvg, GetStorageDeliveryPriceRound(item), MidpointRounding.AwayFromZero);
|
||||
if (item.TradingAmountAvg != roundedPrice)
|
||||
{
|
||||
throw new ServiceException($"期末交割价最多保留{ConsGlobal.SwapDeliveryPriceRound}位小数");
|
||||
}
|
||||
item.TradingAmountAvg = roundedPrice;
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeDeliveryPrices(IEnumerable<swap_flow_event> flowEvents)
|
||||
{
|
||||
if (flowEvents == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in flowEvents.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
item.TradingAmountAvg = Math.Round(item.TradingAmountAvg, GetStorageDeliveryPriceRound(item), MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>保存所有变更(生产: DbContext.SaveChanges;测试: 空操作)</summary>
|
||||
protected virtual void SaveAllChanges()
|
||||
{
|
||||
@@ -1291,7 +1247,6 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
ValidateDeliveryPrices(unwindData);
|
||||
NormalizeNotionalValues(unwindData);
|
||||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||||
@@ -1776,7 +1731,6 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
ValidateDeliveryPrices(unwindData);
|
||||
NormalizeIncomeUnwindDate(unwindData);
|
||||
ValidateIncomeValueDate(unwindData, td);
|
||||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||||
@@ -1817,14 +1771,12 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||||
}
|
||||
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
|
||||
NormalizeDeliveryPrices(swapEvent.unwindData.FlowEvents);
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
NormalizeIncomeUnwindDate(swapEvent.unwindData);
|
||||
ValidateIncomeValueDate(swapEvent.unwindData, td);
|
||||
}
|
||||
var flowList = FindFlowEventsByEventId(swapEvent.id);
|
||||
NormalizeDeliveryPrices(flowList);
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
|
||||
if (swapEvent.unwindData.SwapMarginAmount != 0)
|
||||
@@ -1867,7 +1819,6 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
ValidateDeliveryPrices(unwindData);
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
NormalizeIncomeUnwindDate(unwindData);
|
||||
|
||||
@@ -52,16 +52,6 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
}
|
||||
|
||||
private static decimal ValidateDeliveryPrice(decimal price, string fieldName)
|
||||
{
|
||||
var roundedPrice = Math.Round(price, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero);
|
||||
if (price != roundedPrice)
|
||||
{
|
||||
throw new ServiceException($"{fieldName}最多保留{ConsGlobal.SwapDeliveryPriceRound}位小数");
|
||||
}
|
||||
return roundedPrice;
|
||||
}
|
||||
|
||||
private static decimal? RoundSwapBondNetPriceAndYtm(decimal? value)
|
||||
{
|
||||
return value.HasValue
|
||||
@@ -602,7 +592,7 @@ namespace YLErp.Modules.SwapModule
|
||||
dbTrade.trade_extend = req.trade_extend;
|
||||
dbTrade.swap_positions = req.swap_positions;
|
||||
dbTrade.MetaDic = req.MetaDic;
|
||||
dbTrade.InitYtm = RoundSwapBondNetPriceAndYtm(req.swap_positions.FirstOrDefault(p => p.InitYtm != null)?.InitYtm);
|
||||
dbTrade.InitYtm = req.swap_positions.FirstOrDefault(p => p.InitYtm != null)?.InitYtm;
|
||||
InnerSaveTrade(false, dbTrade, changsStr, changeConfirmStatus);
|
||||
|
||||
return dbTrade;
|
||||
@@ -1381,16 +1371,10 @@ namespace YLErp.Modules.SwapModule
|
||||
position.UnderlyingCode = swap.UnderlyingCode;
|
||||
position.UnderlyingInstrumentType = swap.UnderlyingInstrumentType;
|
||||
position.PosiDirection = swap.PosiDirection;
|
||||
// position.PosiGrossPrice = string.IsNullOrEmpty(swap.UnderlyingCode)
|
||||
// ? Math.Round(swap.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero)
|
||||
// : ValidateDeliveryPrice(swap.PosiGrossPrice, "期初交割价");
|
||||
var storagePriceRound = ConsGlobal.InstrumentType.IsBond(swap.UnderlyingInstrumentType)
|
||||
? ConsGlobal.PriceRound
|
||||
: ConsGlobal.SwapDeliveryPriceRound;
|
||||
position.PosiGrossPrice = Math.Round(swap.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiGrossPrice = swap.PosiGrossPrice;
|
||||
position.PosiNetPrice = swap.PosiQuantity == 0 ? 0 : (position.PosiGrossPrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio);
|
||||
position.PosiNetPrice = Math.Round(position.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiNetNoFeePrice = RoundSwapBondNetPriceAndYtm(swap.PosiNetNoFeePrice);
|
||||
position.PosiNetNoFeePrice = swap.PosiNetNoFeePrice;
|
||||
position.PosiNetFeePrice = swap.PosiQuantity == 0 ? 0 : (swap.PosiNetNoFeePrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio);
|
||||
position.PosiNetFeePrice = Math.Round(position.PosiNetFeePrice??0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiNotionalValue = Math.Round(swap.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
@@ -1416,7 +1400,7 @@ namespace YLErp.Modules.SwapModule
|
||||
position.interest_rest_days = swap.interest_rest_days;
|
||||
position.interest_rule = swap.interest_rule;
|
||||
position.category_tag = string.IsNullOrEmpty(swap.category_tag) ? "互换利率" : swap.category_tag;
|
||||
position.InitYtm = RoundSwapBondNetPriceAndYtm(swap.InitYtm);
|
||||
position.InitYtm = swap.InitYtm;
|
||||
if (position.InitYtm != null && position.InitYtm > 0)
|
||||
{
|
||||
td.InitYtm = position.InitYtm;
|
||||
|
||||
@@ -1,2 +1,125 @@
|
||||
var main = main || {};
|
||||
main.formatOptions = { "trading": { "umprice": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "umpriceP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "umpricePR": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradeSinglePrice": { "trimTailZeros": true, "precision": 2, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "premiumRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "premiumRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradePrice": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "StockEqvNotional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notionalP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "volatility": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "volatilityP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "greek": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "marginRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "marginRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 } } };
|
||||
main.formatOptions={
|
||||
"trading": {
|
||||
"umprice": {
|
||||
"precision": 9,
|
||||
"grouping": true,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 9,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"umpriceP": {
|
||||
"percent": true,
|
||||
"precision": 2,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"umpricePR": {
|
||||
"precision": 4,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 4,
|
||||
"maxDecimals": 2
|
||||
},
|
||||
"tradeSinglePrice": {
|
||||
"precision": 2,
|
||||
"grouping": true,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"premiumRateP": {
|
||||
"percent": true,
|
||||
"precision": 2,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"premiumRate": {
|
||||
"precision": 4,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 4,
|
||||
"maxDecimals": 2
|
||||
},
|
||||
"tradePrice": {
|
||||
"precision": 2,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"StockEqvNotional": {
|
||||
"precision": 2,
|
||||
"grouping": true,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"notional": {
|
||||
"precision": 2,
|
||||
"grouping": true,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"notionalP": {
|
||||
"percent": true,
|
||||
"precision": 2,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"volatility": {
|
||||
"precision": 4,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 4,
|
||||
"maxDecimals": 2
|
||||
},
|
||||
"volatilityP": {
|
||||
"percent": true,
|
||||
"precision": 2,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"greek": {
|
||||
"precision": 2,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"marginRateP": {
|
||||
"percent": true,
|
||||
"precision": 2,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"minDecimals": 2,
|
||||
"maxDecimals": 0
|
||||
},
|
||||
"marginRate": {
|
||||
"precision": 4,
|
||||
"grouping": false,
|
||||
"rounded": true,
|
||||
"percent": false,
|
||||
"minDecimals": 4,
|
||||
"maxDecimals": 2
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
// 通过 /front/swappriceprecision 加载。可按 UnderlyingInstrumentType 修改;缺项或非法值由页面精度组件回退内置默认规则。
|
||||
window.main = window.main || {};
|
||||
window.main.swapPricePrecision = {
|
||||
Stock: { integerDigits: 7, precision: 2 },
|
||||
StockIndex: { integerDigits: 7, precision: 2 },
|
||||
StockIF: { integerDigits: 7, precision: 4 },
|
||||
CommodityFutures: { integerDigits: 7, precision: 4 },
|
||||
CommoditySpot: { integerDigits: 7, precision: 4 },
|
||||
NewOtcStock: { integerDigits: 7, precision: 4 },
|
||||
HKStock: { integerDigits: 7, precision: 4 },
|
||||
HKStockIndex: { integerDigits: 7, precision: 4 },
|
||||
Fund: { integerDigits: 7, precision: 4 },
|
||||
Bond: {
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 6 }
|
||||
},
|
||||
TBonds: {
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 6 }
|
||||
},
|
||||
CreditBonds: {
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 6 }
|
||||
},
|
||||
OtherBonds: {
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 6 }
|
||||
},
|
||||
TBFutures: { integerDigits: 8, precision: 4 },
|
||||
OtherFutures: { integerDigits: 8, precision: 4 },
|
||||
GoldSpot: { integerDigits: 8, precision: 4 },
|
||||
OtherSpot: { integerDigits: 8, precision: 4 },
|
||||
AbroadFutures: { integerDigits: 8, precision: 4 },
|
||||
AbroadSpot: { integerDigits: 8, precision: 4 },
|
||||
AbroadStock: { integerDigits: 8, precision: 2 },
|
||||
AbroadStockIndex: { integerDigits: 8, precision: 4 },
|
||||
ExRate: { integerDigits: 2, precision: 8 },
|
||||
Shibor: { integerDigits: 2, precision: 4 },
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4 },
|
||||
|
||||
// TODO: 利率收益率(6+8)、债券指数(6+4)、黄金期货(6+4)待对应的 UnderlyingInstrumentType 枚举确认后启用。
|
||||
};
|
||||
@@ -37,6 +37,17 @@ namespace YLErp.Web.Controllers
|
||||
return Content(js, "text/javascript");
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||
public ActionResult SwapPricePrecision()
|
||||
{
|
||||
var filePath = Server.MapPath("~/App_Data/Config/swappriceprecision.js");
|
||||
var js = System.IO.File.Exists(filePath)
|
||||
? System.IO.File.ReadAllText(filePath)
|
||||
: "var main = main || {}; main.swapPricePrecision = {};";
|
||||
return Content(js, "text/javascript");
|
||||
}
|
||||
|
||||
//今年及前后两年的日历数据(缓存120s)
|
||||
[ResponseCache(Duration = 120, Location = ResponseCacheLocation.Any)]
|
||||
public ActionResult Calendar()
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<script type="text/javascript">
|
||||
var page = @Json.Serialize(pageObj);
|
||||
</script>
|
||||
<script src="~/front/swappriceprecision?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/EodPositionRisks.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
|
||||
@@ -49,4 +51,4 @@
|
||||
</ul>
|
||||
</div>
|
||||
@Html.Raw(JqGridSimple.OutTable())
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/tradeHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapCalc.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/incomeSwapTrade.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
@@ -163,7 +165,7 @@
|
||||
<!-- 期初标的交割净价: TradingAmountNetAvg 字段名为"成交净价(期末语义)",但此处后端 InitIncome 实际装入的是期初净价(position.PosiNetNoFeePrice),值是期初值 -->
|
||||
<td v-if="deal.StructureType!='普通收益互换'">{{priceFormat(floatPosition.TradingAmountNetAvg > 0 ? floatPosition.TradingAmountNetAvg : floatPosition.PosiNetPrice)}}</td>
|
||||
<td>
|
||||
<vue-number-input v-model="floatPosition.TradingAmountAvg" v-bind:format="inputFormatSwapDeliveryPrice" v-on:input="changeUnderlyingPrice" style="width:107px;"></vue-number-input>
|
||||
<vue-swap-price-input v-model="floatPosition.TradingAmountAvg" v-bind:format="getDeliveryPriceInputFormat()" v-on:input="changeUnderlyingPrice" style="width:107px;"></vue-swap-price-input>
|
||||
<a href="javascript:void(0)" v-on:click="refreshUnderlyingPrice()">
|
||||
<span title="使用系统标的价格" class="glyphicon glyphicon-refresh"></span>
|
||||
</a>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/tradeHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapCalc.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/unwindSwapTrade.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
@@ -200,7 +202,7 @@
|
||||
</td>
|
||||
<td>{{priceFormat(floatPosition.PosiGrossPrice)}}</td>
|
||||
<td>
|
||||
<vue-number-input v-model="floatPosition.TradingAmountAvg" v-bind:format="inputFormatSwapDeliveryPrice" v-on:input="changeUnderlyingPrice" style="width:107px;"></vue-number-input>
|
||||
<vue-swap-price-input v-model="floatPosition.TradingAmountAvg" v-bind:format="getDeliveryPriceInputFormat()" v-on:input="changeUnderlyingPrice" style="width:107px;"></vue-swap-price-input>
|
||||
<a href="javascript:void(0)" v-on:click="refreshUnderlyingPrice()">
|
||||
<span title="使用系统标的价格" class="glyphicon glyphicon-refresh"></span>
|
||||
</a>
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/tradeHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapCalc.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapTradeEdit.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
@@ -474,16 +476,16 @@
|
||||
</a>
|
||||
</td>
|
||||
<td v-if="trade.StructureType!='普通收益互换'">
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="inputFormatSwapBondDeliveryPrice" v-on:input="onDpPriceInput(item)"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='DP'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.DP" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span>
|
||||
<vue-swap-price-input :key="getPosiPriceFormatKey(item,'PosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="getPosiPriceInputFormat(item,'grossPrice')" v-on:input="onDpPriceInput(item)"></vue-swap-price-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='DP'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.DP" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span>
|
||||
</td>
|
||||
<td v-if="trade.StructureType!='普通收益互换'">
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiNetNoFeePrice')" v-model="item.PosiNetNoFeePrice" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceInput(item,'CP')"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='CP'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.CP" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span>
|
||||
<vue-swap-price-input :key="getPosiPriceFormatKey(item,'PosiNetNoFeePrice')" v-model="item.PosiNetNoFeePrice" v-bind:format="getPosiPriceInputFormat(item,'netPrice')" v-on:input="onBondPriceInput(item,'CP')"></vue-swap-price-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='CP'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.CP" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span>
|
||||
</td>
|
||||
<td v-if="trade.StructureType!='普通收益互换'">
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'InitYtm')" v-model="item.InitYtm" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceInput(item,'YD')"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='YD'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.YD" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span><a href="javascript:void(0);" v-on:click="resetBondCalc(item)" v-if="item.isBond" style="margin-left:6px;font-size:11px;color:#1565c0;">重算</a>
|
||||
<vue-swap-price-input :key="getPosiPriceFormatKey(item,'InitYtm')" v-model="item.InitYtm" v-bind:format="getPosiPriceInputFormat(item,'yield')" v-on:input="onBondPriceInput(item,'YD')"></vue-swap-price-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='YD'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.YD" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span><a href="javascript:void(0);" v-on:click="resetBondCalc(item)" v-if="item.isBond" style="margin-left:6px;font-size:11px;color:#1565c0;">重算</a>
|
||||
</td>
|
||||
<td v-if="trade.StructureType=='普通收益互换'">
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'normalPosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="inputFormatSwapDeliveryPrice" v-on:input="changeSpotPrice(item)"></vue-number-input>
|
||||
<vue-swap-price-input :key="getPosiPriceFormatKey(item,'normalPosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="getPosiPriceInputFormat(item,'grossPrice')" v-on:input="changeSpotPrice(item)"></vue-swap-price-input>
|
||||
</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.PosiQuantity" v-on:input="changeQuantity(item)" v-bind:format="inputFormatTradeAmount"></vue-number-input>{{item.underlying!=null?item.underlying.QuoteUnitString:''}}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using YLErp.Enums;
|
||||
@using System.Globalization;
|
||||
@model TradeViewModel
|
||||
|
||||
@{
|
||||
@@ -29,6 +30,7 @@
|
||||
var positions = trade.swap_positions.Where(x=>x.PosiDirection>0&&x.IsInitial).ToList();
|
||||
var realPositions = trade.swap_positions.Where(x => x.PosiDirection > 0 && !x.IsInitial).ToList();
|
||||
var sr = trade.trade_extend.ExtendObj.SettlementRules;
|
||||
string SwapPriceData(decimal? value) => value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
}
|
||||
@section CSS{
|
||||
<link href="~/Style/Css/swapTradeView.css?@HtmlUtil.JsVersion" rel="stylesheet" />
|
||||
@@ -46,6 +48,8 @@
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/tradeHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapTradeView.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
|
||||
@@ -396,15 +400,15 @@
|
||||
<td>@(item.PositionType == (int)PositionTypeFlag.Long ? "多头" : "空头")</td>
|
||||
<td>@item.UnderlyingCode</td>
|
||||
<td>
|
||||
@((item.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice))
|
||||
<span class="js-swap-price" data-value="@SwapPriceData(item.PosiGrossPrice * multiplier)" data-instrument-type="@item.UnderlyingInstrumentType" data-field="grossPrice"></span>
|
||||
</td>
|
||||
@if (isBond)
|
||||
{
|
||||
<td>
|
||||
@(((item.PosiNetNoFeePrice ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umprice))
|
||||
<span class="js-swap-price" data-value="@SwapPriceData((item.PosiNetNoFeePrice ?? 0) * multiplier)" data-instrument-type="@item.UnderlyingInstrumentType" data-field="netPrice"></span>
|
||||
</td>
|
||||
<td>
|
||||
@((item.InitYtm * multiplier).OtcFormat(OtcFormatFlag.umprice))
|
||||
<span class="js-swap-price" data-value="@SwapPriceData(item.InitYtm * multiplier)" data-instrument-type="@item.UnderlyingInstrumentType" data-field="yield"></span>
|
||||
</td>
|
||||
}
|
||||
<td>
|
||||
@@ -613,12 +617,12 @@
|
||||
</td>
|
||||
<td>@item.UnderlyingCode</td>
|
||||
<td>
|
||||
@((item.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice))
|
||||
<span class="js-swap-price" data-value="@SwapPriceData(item.PosiGrossPrice * multiplier)" data-instrument-type="@item.UnderlyingInstrumentType" data-field="grossPrice"></span>
|
||||
</td>
|
||||
@if (isBond)
|
||||
{
|
||||
<td>
|
||||
@(((item.PosiNetNoFeePrice ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umprice))
|
||||
<span class="js-swap-price" data-value="@SwapPriceData((item.PosiNetNoFeePrice ?? 0) * multiplier)" data-instrument-type="@item.UnderlyingInstrumentType" data-field="netPrice"></span>
|
||||
</td>
|
||||
}
|
||||
<td>
|
||||
@@ -818,13 +822,13 @@
|
||||
<td>@closeFloat.UnderlyingCode</td>
|
||||
@if (isBond)
|
||||
{
|
||||
<td>@((closeFloat.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice))</td>
|
||||
<td>@((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice))</td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.PosiGrossPrice * multiplier)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.TradingAmountAvg * multiplier)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td>@(closeFloat.PosiGrossPrice.OtcFormat(OtcFormatFlag.umprice))</td>
|
||||
<td>@(closeFloat.TradingAmountAvg.OtcFormat(OtcFormatFlag.umprice))</td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.PosiGrossPrice)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.TradingAmountAvg)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
}
|
||||
<td>@(closeFloat.Quantity.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.TradingFee.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
@@ -1014,14 +1018,14 @@
|
||||
<td>@closeFloat.UnderlyingCode</td>
|
||||
@if (isBond)
|
||||
{
|
||||
<td>@((closeFloat.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice))</td>
|
||||
<td>@(((closeFloat.TradingAmountNetAvg ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umprice))</td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.PosiGrossPrice * multiplier)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData((closeFloat.TradingAmountNetAvg ?? 0) * multiplier)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="netPrice"></span></td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td>@((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice))</td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.TradingAmountAvg * multiplier)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
}
|
||||
<td>@((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice))</td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.TradingAmountAvg * multiplier)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
<td>@((closeFloat.PositionQty??0).OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.TradingFee.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.DividendIn.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
|
||||
@@ -800,7 +800,10 @@ function exportEodSwapRows(jgrid, fileName, groupConfig, exportColumnNames) {
|
||||
}
|
||||
//---------------------------Formatter---------------------------------
|
||||
function PriceFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.umprice(cellValue);
|
||||
return swapPricePrecision.format(
|
||||
cellValue,
|
||||
rowObject && rowObject.eodPosition && rowObject.eodPosition.UnderlyingInstrumentType,
|
||||
'grossPrice');
|
||||
}
|
||||
|
||||
function RealizedPnlFormat(cellValue, options, rowObject) {
|
||||
|
||||
@@ -73,14 +73,18 @@ const vue = new Vue({
|
||||
$(this.$refs.incomeValueDatePicker.$el).val(MaxIncomeValueDate);
|
||||
return false;
|
||||
},
|
||||
// 守卫: 价格缩放因子(债券 multiplier=100 时界面为百分比态, 计算用相对价需 ÷100)
|
||||
// 计算已外置到 swapCalc.getPriceScale; 改动需同步 swapCalc.test.js
|
||||
getPriceScale() {
|
||||
return SwapCalc.getPriceScale(this.multiplier);
|
||||
getDeliveryPriceInputFormat() {
|
||||
return swapPricePrecision.getInputFormat(
|
||||
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
|
||||
'grossPrice',
|
||||
inputFormatSwapDeliveryPrice);
|
||||
},
|
||||
getStorageDeliveryPrice() {
|
||||
const precision = inputFormatSwapDeliveryPrice.precision + (this.multiplier === 100 ? 2 : 0);
|
||||
return SwapCalc.roundHalfAwayFromZero(Number(this.floatPosition.TradingAmountAvg) * this.getPriceScale(), precision);
|
||||
return swapPricePrecision.roundForSubmit(
|
||||
swapPricePrecision.shiftDecimal(this.floatPosition.TradingAmountAvg, this.multiplier === 100 ? -2 : 0),
|
||||
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
|
||||
'grossPrice',
|
||||
this.multiplier === 100 ? 2 : 0);
|
||||
},
|
||||
initDeal() {
|
||||
var positions = model.FlowEvents.filter((item) => {
|
||||
@@ -110,8 +114,8 @@ const vue = new Vue({
|
||||
return tradeHelper.IsBond(instType);
|
||||
},
|
||||
priceFormat(price) {
|
||||
price = price * this.multiplier;
|
||||
var pricef = otcformat.trading.umprice(price);
|
||||
price = swapPricePrecision.shiftDecimal(price, this.multiplier === 100 ? 2 : 0);
|
||||
var pricef = swapPricePrecision.format(price, this.floatPosition && this.floatPosition.UnderlyingInstrumentType, 'grossPrice');
|
||||
return pricef;
|
||||
},
|
||||
dataFormat() {
|
||||
@@ -165,8 +169,10 @@ const vue = new Vue({
|
||||
main.post("/underlying_manager/GetUnderlyingPriceByCode",
|
||||
{ code: thisObj.floatPosition.UnderlyingCode, valuedate: thisObj.deal.ValueDate })
|
||||
.done(function (res) {
|
||||
res.obj = res.obj * thisObj.multiplier;
|
||||
thisObj.floatPosition.TradingAmountAvg = _.round(Number(res.obj), 9);
|
||||
thisObj.floatPosition.TradingAmountAvg = swapPricePrecision.roundForSubmit(
|
||||
swapPricePrecision.shiftDecimal(res.obj, thisObj.multiplier === 100 ? 2 : 0),
|
||||
thisObj.floatPosition.UnderlyingInstrumentType,
|
||||
'grossPrice');
|
||||
thisObj.calcFloatClosePnl();
|
||||
});
|
||||
},
|
||||
@@ -382,6 +388,7 @@ const vue = new Vue({
|
||||
components: {
|
||||
'vue-datepicker': FastVue.vueDatePicker(),
|
||||
'vue-number-input': FastVue.vueNumberInput(),
|
||||
'vue-swap-price-input': swapPricePrecision.createVueInputComponent(),
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
var swapPricePrecision = (function (global) {
|
||||
const defaults = Object.freeze({
|
||||
Stock: { integerDigits: 7, precision: 2 },
|
||||
StockIndex: { integerDigits: 7, precision: 2 },
|
||||
StockIF: { integerDigits: 7, precision: 4 },
|
||||
CommodityFutures: { integerDigits: 7, precision: 4 },
|
||||
CommoditySpot: { integerDigits: 7, precision: 4 },
|
||||
NewOtcStock: { integerDigits: 7, precision: 4 },
|
||||
HKStock: { integerDigits: 7, precision: 4 },
|
||||
HKStockIndex: { integerDigits: 7, precision: 4 },
|
||||
Fund: { integerDigits: 7, precision: 4 },
|
||||
Bond: {
|
||||
grossPrice: { integerDigits: 6, precision: 11 },
|
||||
netPrice: { integerDigits: 6, precision: 11 },
|
||||
yield: { integerDigits: 2, precision: 6 }
|
||||
},
|
||||
TBonds: {
|
||||
grossPrice: { integerDigits: 6, precision: 11 },
|
||||
netPrice: { integerDigits: 6, precision: 11 },
|
||||
yield: { integerDigits: 2, precision: 6 }
|
||||
},
|
||||
CreditBonds: {
|
||||
grossPrice: { integerDigits: 6, precision: 11 },
|
||||
netPrice: { integerDigits: 6, precision: 11 },
|
||||
yield: { integerDigits: 2, precision: 6 }
|
||||
},
|
||||
OtherBonds: {
|
||||
grossPrice: { integerDigits: 6, precision: 11 },
|
||||
netPrice: { integerDigits: 6, precision: 11 },
|
||||
yield: { integerDigits: 2, precision: 6 }
|
||||
},
|
||||
TBFutures: { integerDigits: 8, precision: 4 },
|
||||
OtherFutures: { integerDigits: 8, precision: 4 },
|
||||
GoldSpot: { integerDigits: 8, precision: 4 },
|
||||
OtherSpot: { integerDigits: 8, precision: 4 },
|
||||
AbroadFutures: { integerDigits: 8, precision: 4 },
|
||||
AbroadSpot: { integerDigits: 8, precision: 4 },
|
||||
AbroadStock: { integerDigits: 8, precision: 2 },
|
||||
AbroadStockIndex: { integerDigits: 8, precision: 4 },
|
||||
ExRate: { integerDigits: 2, precision: 8 },
|
||||
Shibor: { integerDigits: 2, precision: 4 },
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4 }
|
||||
// TODO: Add InterestYield, BondIndex and GoldFutures after their enum values are confirmed.
|
||||
});
|
||||
|
||||
function normalizeDecimal(value) {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
let text = String(value).trim();
|
||||
if (/[eE]/.test(text)) {
|
||||
const number = Number(text);
|
||||
if (!Number.isFinite(number)) return null;
|
||||
text = number.toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
|
||||
}
|
||||
if (!/^[+-]?(?:\d+|\d*\.\d+)$/.test(text)) return null;
|
||||
const negative = text.charAt(0) === '-';
|
||||
text = text.replace(/^[+-]/, '');
|
||||
const parts = text.split('.');
|
||||
const integerPart = parts[0].replace(/^0+(?=\d)/, '') || '0';
|
||||
const decimalPart = parts.length > 1 ? parts[1] : '';
|
||||
const result = integerPart + (decimalPart ? '.' + decimalPart : '');
|
||||
return negative && !/^0(?:\.0*)?$/.test(result) ? '-' + result : result;
|
||||
}
|
||||
|
||||
function shiftDecimal(value, places) {
|
||||
let normalized = normalizeDecimal(value);
|
||||
if (!normalized || !Number.isInteger(places) || places === 0) return normalized;
|
||||
const negative = normalized.charAt(0) === '-';
|
||||
const parts = (negative ? normalized.substring(1) : normalized).split('.');
|
||||
const integerPart = parts[0];
|
||||
const decimalPart = parts.length > 1 ? parts[1] : '';
|
||||
const digits = integerPart + decimalPart;
|
||||
const decimalIndex = integerPart.length + places;
|
||||
let text;
|
||||
if (decimalIndex <= 0) text = '0.' + '0'.repeat(-decimalIndex) + digits;
|
||||
else if (decimalIndex >= digits.length) text = digits + '0'.repeat(decimalIndex - digits.length);
|
||||
else text = digits.substring(0, decimalIndex) + '.' + digits.substring(decimalIndex);
|
||||
return normalizeDecimal((negative ? '-' : '') + text);
|
||||
}
|
||||
|
||||
function incrementDigits(value) {
|
||||
let carry = 1;
|
||||
let result = '';
|
||||
for (let index = value.length - 1; index >= 0; index--) {
|
||||
const digit = value.charCodeAt(index) - 48 + carry;
|
||||
if (digit === 10) {
|
||||
result = '0' + result;
|
||||
carry = 1;
|
||||
} else {
|
||||
result = String(digit) + result;
|
||||
carry = 0;
|
||||
}
|
||||
}
|
||||
return carry ? '1' + result : result;
|
||||
}
|
||||
|
||||
function roundDecimal(value, precision) {
|
||||
const normalized = normalizeDecimal(value);
|
||||
if (normalized === null || !Number.isInteger(precision) || precision < 0) return value;
|
||||
const negative = normalized.charAt(0) === '-';
|
||||
const parts = (negative ? normalized.substring(1) : normalized).split('.');
|
||||
let integerPart = parts[0];
|
||||
const decimalPart = parts.length > 1 ? parts[1] : '';
|
||||
if (decimalPart.length <= precision) return normalized;
|
||||
|
||||
let digits = integerPart + decimalPart.substring(0, precision);
|
||||
if (decimalPart.charAt(precision) >= '5') digits = incrementDigits(digits);
|
||||
if (digits.length <= precision) digits = digits.padStart(precision + 1, '0');
|
||||
integerPart = precision === 0 ? digits : digits.substring(0, digits.length - precision);
|
||||
const roundedDecimal = precision === 0 ? '' : digits.substring(digits.length - precision);
|
||||
return normalizeDecimal((negative ? '-' : '') + integerPart + (roundedDecimal ? '.' + roundedDecimal : ''));
|
||||
}
|
||||
|
||||
function normalizeRule(rule) {
|
||||
if (!rule || typeof rule !== 'object') return null;
|
||||
const integerDigits = Number(rule.integerDigits);
|
||||
const precision = Number(rule.precision);
|
||||
if (!Number.isInteger(integerDigits) || integerDigits < 1 || integerDigits > 18
|
||||
|| !Number.isInteger(precision) || precision < 0 || precision > 13) return null;
|
||||
return { integerDigits: integerDigits, precision: precision };
|
||||
}
|
||||
|
||||
function findRule(source, instrumentType, field) {
|
||||
const typeRule = source && source[instrumentType];
|
||||
return typeRule ? normalizeRule(typeRule[field] || typeRule) : null;
|
||||
}
|
||||
|
||||
function getRule(instrumentType, field) {
|
||||
const fallback = findRule(defaults, instrumentType, field);
|
||||
if (!fallback) return null;
|
||||
return findRule(global.main && global.main.swapPricePrecision, instrumentType, field) || fallback;
|
||||
}
|
||||
|
||||
function format(value, instrumentType, field) {
|
||||
const rule = getRule(instrumentType, field);
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
if (!rule) return global.otcformat.trading.umprice(value);
|
||||
const rounded = roundDecimal(value, rule.precision);
|
||||
return rounded === null ? '' : rounded.replace(/(\.\d*?[1-9])0+$/, '$1').replace(/\.0+$/, '');
|
||||
}
|
||||
|
||||
function normalizeInput(value, format, shouldRound) {
|
||||
const options = format || {};
|
||||
const maxIntegerDigits = Number(options.integerDigits) || 0;
|
||||
const precision = Number(options.precision) || 0;
|
||||
const source = String(value === null || value === undefined ? '' : value).trim().replaceAll(',', '').replaceAll('。', '.');
|
||||
let negative = false;
|
||||
let hasDot = false;
|
||||
let integerPart = '';
|
||||
let decimalPart = '';
|
||||
for (let index = 0; index < source.length; index++) {
|
||||
const ch = source.charAt(index);
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
if (hasDot) {
|
||||
if (shouldRound || decimalPart.length < precision) decimalPart += ch;
|
||||
} else if (!maxIntegerDigits || integerPart.length < maxIntegerDigits) {
|
||||
integerPart += ch;
|
||||
}
|
||||
} else if (ch === '.' && !hasDot && precision > 0) {
|
||||
hasDot = true;
|
||||
} else if (ch === '-' && index === 0 && options.negative) {
|
||||
negative = true;
|
||||
}
|
||||
}
|
||||
if (!integerPart && !decimalPart) return negative ? '-' : '';
|
||||
const text = (negative ? '-' : '') + (integerPart || '0') + (hasDot ? '.' + decimalPart : '');
|
||||
if (!shouldRound || text.endsWith('.')) return text;
|
||||
return roundDecimal(text, precision);
|
||||
}
|
||||
|
||||
function createVueInputComponent() {
|
||||
return {
|
||||
props: {
|
||||
value: { type: [Number, String], default: '' },
|
||||
format: { type: Object, default: function () { return {}; } },
|
||||
disabled: { type: Boolean }
|
||||
},
|
||||
data: function () {
|
||||
return { text: '' };
|
||||
},
|
||||
mounted: function () {
|
||||
this.text = this.toDisplay(this.value);
|
||||
},
|
||||
methods: {
|
||||
toDisplay: function (value) {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
const displayValue = this.format && this.format.percent ? shiftDecimal(value, 2) : String(value);
|
||||
return normalizeInput(displayValue, this.format, true);
|
||||
},
|
||||
toModel: function (value) {
|
||||
if (!value || value === '-') return '';
|
||||
return this.format && this.format.percent ? shiftDecimal(value, -2) : value;
|
||||
},
|
||||
updateValue: function (value, shouldRound, shouldCommit) {
|
||||
this.text = normalizeInput(value, this.format, shouldRound);
|
||||
if (this.text.endsWith('.') && !shouldRound) return;
|
||||
if (shouldCommit) this.$emit('input', this.toModel(this.text));
|
||||
},
|
||||
onInput: function (event) {
|
||||
this.updateValue(event.target.value, false, false);
|
||||
// When the normalized value is unchanged, Vue skips the DOM patch.
|
||||
// Write it directly so excess digits do not remain in the native input.
|
||||
event.target.value = this.text;
|
||||
},
|
||||
onPaste: function (event) {
|
||||
const clipboard = event.clipboardData || global.clipboardData;
|
||||
if (!clipboard) return;
|
||||
event.preventDefault();
|
||||
this.updateValue(clipboard.getData('text'), true, false);
|
||||
event.target.value = this.text;
|
||||
},
|
||||
onBlur: function () {
|
||||
if (this.text.endsWith('.')) this.text = this.text.substring(0, this.text.length - 1);
|
||||
this.updateValue(this.text, true, true);
|
||||
event.target.value = this.text;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function (value) {
|
||||
const display = this.toDisplay(value);
|
||||
if (display !== this.text) this.text = display;
|
||||
},
|
||||
format: {
|
||||
deep: true,
|
||||
handler: function () {
|
||||
this.text = this.toDisplay(this.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<input type="text" :disabled="disabled" :value="text" @input="onInput" @paste="onPaste" @blur="onBlur">'
|
||||
};
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
getRule: getRule,
|
||||
getInputFormat: function (instrumentType, field, options) {
|
||||
const rule = getRule(instrumentType, field);
|
||||
return rule ? Object.assign({}, options, rule) : Object.assign({}, options);
|
||||
},
|
||||
format: format,
|
||||
roundForSubmit: function (value, instrumentType, field, storagePrecisionOffset) {
|
||||
const rule = getRule(instrumentType, field);
|
||||
if (value === null || value === undefined || value === '' || !rule) return value;
|
||||
const offset = Number.isInteger(storagePrecisionOffset) ? storagePrecisionOffset : 0;
|
||||
return roundDecimal(value, rule.precision + offset);
|
||||
},
|
||||
shiftDecimal: shiftDecimal,
|
||||
createVueInputComponent: createVueInputComponent
|
||||
});
|
||||
}(window));
|
||||
@@ -16,9 +16,6 @@ const inputFormatPosiFeePercent = Object.freeze({ precision: 4, negative: true,
|
||||
const inputFormatPosiFeeUnit = Object.freeze({ precision: 6, negative: true, append: '' });
|
||||
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
|
||||
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '', percent: false });
|
||||
const inputFormatSwapBondDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '', percent: true });
|
||||
const inputFormatSwapBondNetPriceAndYtm = Object.freeze({ precision: 9, negative: true, append: '', percent: true });
|
||||
const swapBondStoragePricePrecision = inputFormatSwapBondDeliveryPrice.precision + 2;
|
||||
const consPosiFeeType = Object.freeze({ Percent: 0, Unit: 1 });
|
||||
const swapPosiFeeCalc = Object.freeze({
|
||||
normalizeFeeType(feeType) {
|
||||
@@ -251,19 +248,26 @@ const vue = new Vue({
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
roundStorageDeliveryPrice(item, price) {
|
||||
const precision = tradeHelper.IsBond(item && item.UnderlyingInstrumentType)
|
||||
? swapBondStoragePricePrecision
|
||||
: inputFormatSwapDeliveryPrice.precision;
|
||||
return _.round(Number(price), precision);
|
||||
getPosiPriceInputFormat(item, field) {
|
||||
return swapPricePrecision.getInputFormat(
|
||||
item && item.UnderlyingInstrumentType,
|
||||
field,
|
||||
Object.assign({}, inputFormatSwapDeliveryPrice, {
|
||||
percent: this.trade.StructureType !== '普通收益互换'
|
||||
}));
|
||||
},
|
||||
roundStorageBondNetPriceAndYtm(value) {
|
||||
return value == null ? value : _.round(Number(value), swapBondStoragePricePrecision);
|
||||
roundStoragePrice(item, price, field) {
|
||||
const storagePrecisionOffset = tradeHelper.IsBond(item && item.UnderlyingInstrumentType) ? 2 : 0;
|
||||
return swapPricePrecision.roundForSubmit(
|
||||
price,
|
||||
item && item.UnderlyingInstrumentType,
|
||||
field,
|
||||
storagePrecisionOffset);
|
||||
},
|
||||
getPosiPriceFormatKey(item, field) {
|
||||
const index = item && item.index != null ? item.index : '';
|
||||
const isBond = tradeHelper.IsBond(item && item.UnderlyingInstrumentType);
|
||||
return `${index}-${field}-${isBond ? 'bond' : 'other'}`;
|
||||
const instrumentType = item && item.UnderlyingInstrumentType ? item.UnderlyingInstrumentType : 'unknown';
|
||||
return `${index}-${field}-${instrumentType}`;
|
||||
},
|
||||
getCurrentPosiFeeType() {
|
||||
return this.posiFeeModePercent ? consPosiFeeType.Percent : consPosiFeeType.Unit;
|
||||
@@ -404,7 +408,7 @@ const vue = new Vue({
|
||||
return;
|
||||
}
|
||||
item._lastBondErr = null; // 成功则清标记,便于下次真出不同错误时仍能提示
|
||||
// 以既有三字段为代理,调用纯函数(已手动设过的字段不被覆盖),再写回。
|
||||
// 以既有三字段为代理,调用纯函数;手工输入字段保留原始十进制字符串,避免经 Number 参与计算后丢失末位。
|
||||
// 关键:proxy 内必须统一为【展示态】(per-100-face),因为 applyBondCalcResult 写入的是计算器返回的展示态。
|
||||
// 模型字段是【存储态小数】(percent:true 下 1.00 对应界面 100),所以初始化时要 bondPriceToCalc(×100);
|
||||
// 若直接用存储态初始化,则用户手填字段被 applyBondCalcResult 跳过后,proxy 中仍残留存储态,
|
||||
@@ -418,9 +422,9 @@ const vue = new Vue({
|
||||
SwapCalc.applyBondCalcResult(proxy, resp.obj, manual);
|
||||
// 回写前 bondCalcPriceToStorage(÷100)(展示态→存储态小数):proxy 里均为展示态;
|
||||
// 模型字段存存储态(0.995),须 ÷100 落回模型,否则配合 percent:true 显示会 ×100 成离谱值。
|
||||
item.PosiNetNoFeePrice = SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice);
|
||||
item.PosiGrossPrice = SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice);
|
||||
item.InitYtm = SwapCalc.bondCalcPriceToStorage(proxy.ytm);
|
||||
if (!manual.CP) item.PosiNetNoFeePrice = self.roundStoragePrice(item, SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice), 'netPrice');
|
||||
if (!manual.DP) item.PosiGrossPrice = self.roundStoragePrice(item, SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice), 'grossPrice');
|
||||
if (!manual.YD) item.InitYtm = self.roundStoragePrice(item, SwapCalc.bondCalcPriceToStorage(proxy.ytm), 'yield');
|
||||
// 名义本金依赖全价(PosiGrossPrice):以净价/收益率为源反算出的全价被回写后,
|
||||
// 直接赋值不会触发组件 input 事件,需在此显式重算,保持名义本金与全价一致。
|
||||
if (self.calcNotional) self.calcNotional();
|
||||
@@ -453,7 +457,6 @@ const vue = new Vue({
|
||||
//计算数量
|
||||
// if (this.paySwapList.length > 0) {
|
||||
// var item = this.paySwapList[0];
|
||||
// var deliveryPrice = this.roundStorageDeliveryPrice(item, item.PosiGrossPrice);
|
||||
// var notional = deliveryPrice * item.ContractSize;
|
||||
// item.PosiQuantity = notional == 0 ? 0 : _.round(this.trade.StockEqvNotional / notional, page.otcFormatConfig.StockEqvNotional.precision);
|
||||
// this.calcNotional();
|
||||
@@ -535,7 +538,7 @@ const vue = new Vue({
|
||||
}
|
||||
var national = payItem.PosiQuantity * payItem.ContractSize;
|
||||
// 守卫: 名义本金必须 round 到 2 位 → 对应历史 bug f873239a(缺 _.round); 外置到 swapCalc.calcStockEqvNotional
|
||||
var deliveryPrice = this.roundStorageDeliveryPrice(payItem, payItem.PosiGrossPrice);
|
||||
var deliveryPrice = this.roundStoragePrice(payItem, payItem.PosiGrossPrice, 'grossPrice');
|
||||
var stockEqvNotional = SwapCalc.calcStockEqvNotional(deliveryPrice, national);//名义本金=期初价格*数量*乘数
|
||||
this.trade.StockEqvNotional = otcformat.trading.stockEqvNotional(stockEqvNotional);
|
||||
payItem.PosiNotionalValue = this.trade.StockEqvNotional;
|
||||
@@ -682,9 +685,9 @@ const vue = new Vue({
|
||||
errorcount++;
|
||||
return false;
|
||||
}
|
||||
x.PosiGrossPrice = thisObj.roundStorageDeliveryPrice(x, x.PosiGrossPrice);
|
||||
x.PosiNetNoFeePrice = thisObj.roundStorageBondNetPriceAndYtm(x.PosiNetNoFeePrice);
|
||||
x.InitYtm = x.InitYtm == null ? null : thisObj.roundStorageBondNetPriceAndYtm(x.InitYtm);
|
||||
x.PosiGrossPrice = thisObj.roundStoragePrice(x, x.PosiGrossPrice, 'grossPrice');
|
||||
x.PosiNetNoFeePrice = thisObj.roundStoragePrice(x, x.PosiNetNoFeePrice, 'netPrice');
|
||||
x.InitYtm = x.InitYtm == null ? null : thisObj.roundStoragePrice(x, x.InitYtm, 'yield');
|
||||
x.PosiFeeType = thisObj.normalizePosiFeeType(x.PosiFeeType);
|
||||
thisObj.trade.swap_positions.push(x);
|
||||
});
|
||||
@@ -820,8 +823,8 @@ const vue = new Vue({
|
||||
// 非 EodPrice 分支 ×bondPriceMultiple=0.01)。故此处仅做精度格式化,**不可**再 bondCalcPriceToStorage(÷100),
|
||||
// 否则默认价 1.0 被除成 0.01,界面 percent:true 再 ×100 显示为 1("被自动除以100"bug)。
|
||||
// 计算器(/Bond/CalcBond)返回的才是展示态,其 ÷100 落库逻辑在 calcBondForItem 内处理。
|
||||
item.PosiNetNoFeePrice = thisObj.roundStorageBondNetPriceAndYtm(resp.obj.netPrice);
|
||||
item.PosiGrossPrice = thisObj.roundStorageDeliveryPrice(item, resp.obj.price);
|
||||
item.PosiNetNoFeePrice = thisObj.roundStoragePrice(item, resp.obj.netPrice, 'netPrice');
|
||||
item.PosiGrossPrice = thisObj.roundStoragePrice(item, resp.obj.price, 'grossPrice');
|
||||
thisObj.calcNotional();
|
||||
});
|
||||
},
|
||||
@@ -1739,6 +1742,7 @@ const vue = new Vue({
|
||||
components: {
|
||||
'vue-datepicker': FastVue.vueDatePicker(),
|
||||
'vue-number-input': FastVue.vueNumberInput(),
|
||||
'vue-swap-price-input': swapPricePrecision.createVueInputComponent(),
|
||||
'vue-underlying-nonbond': vueUnderlyingNonBond(),
|
||||
'vue-underlying-bond': vueUnderlyingBond(),
|
||||
'vue-underlying-rate': vueUnderlyingRate()
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
const inputFormatDouble2 = Object.freeze({ precision: 2, append: '' });
|
||||
|
||||
function formatSwapPriceElements() {
|
||||
$('.js-swap-price').each(function () {
|
||||
this.textContent = swapPricePrecision.format(
|
||||
this.dataset.value,
|
||||
this.dataset.instrumentType,
|
||||
this.dataset.field);
|
||||
});
|
||||
}
|
||||
|
||||
function deletetrade(id) { //无效化
|
||||
main.confirm(page.ConfirmInfo, function () {
|
||||
$.ajax({
|
||||
@@ -541,6 +550,7 @@ function chk_onclick(obj) {
|
||||
}
|
||||
|
||||
$(function () {
|
||||
formatSwapPriceElements();
|
||||
refreshEntryExit();
|
||||
});
|
||||
|
||||
|
||||
@@ -54,12 +54,18 @@ const vue = new Vue({
|
||||
this.setUnwindDate();
|
||||
},
|
||||
methods: {
|
||||
getPriceScale() {
|
||||
return this.multiplier == 100 ? 0.01 : 1;
|
||||
getDeliveryPriceInputFormat() {
|
||||
return swapPricePrecision.getInputFormat(
|
||||
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
|
||||
'grossPrice',
|
||||
inputFormatSwapDeliveryPrice);
|
||||
},
|
||||
getStorageDeliveryPrice() {
|
||||
const precision = inputFormatSwapDeliveryPrice.precision + (this.multiplier === 100 ? 2 : 0);
|
||||
return SwapCalc.roundHalfAwayFromZero(Number(this.floatPosition.TradingAmountAvg) * this.getPriceScale(), precision);
|
||||
return swapPricePrecision.roundForSubmit(
|
||||
swapPricePrecision.shiftDecimal(this.floatPosition.TradingAmountAvg, this.multiplier === 100 ? -2 : 0),
|
||||
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
|
||||
'grossPrice',
|
||||
this.multiplier === 100 ? 2 : 0);
|
||||
},
|
||||
initDeal() {
|
||||
var positions = model.FlowEvents.filter((item) => {
|
||||
@@ -81,15 +87,15 @@ const vue = new Vue({
|
||||
? this.deal.PosiNotionalValue / this.deal.NotionalValue : 1;
|
||||
// 转换期末标的价格为百分比形式
|
||||
if (this.floatPosition.TradingAmountAvg) {
|
||||
this.floatPosition.TradingAmountAvg = this.floatPosition.TradingAmountAvg * this.multiplier;
|
||||
this.floatPosition.TradingAmountAvg = swapPricePrecision.shiftDecimal(this.floatPosition.TradingAmountAvg, this.multiplier === 100 ? 2 : 0);
|
||||
}
|
||||
},
|
||||
IsBond(instType) {
|
||||
return tradeHelper.IsBond(instType);
|
||||
},
|
||||
priceFormat(price) {
|
||||
price = price * this.multiplier;
|
||||
var pricef = otcformat.trading.umprice(price);
|
||||
price = swapPricePrecision.shiftDecimal(price, this.multiplier === 100 ? 2 : 0);
|
||||
var pricef = swapPricePrecision.format(price, this.floatPosition && this.floatPosition.UnderlyingInstrumentType, 'grossPrice');
|
||||
return pricef;
|
||||
},
|
||||
dataFormat() {
|
||||
@@ -105,7 +111,10 @@ const vue = new Vue({
|
||||
this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
|
||||
//this.floatPosition.PosiNetPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiNetPrice);
|
||||
//this.floatPosition.PosiGrossPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiGrossPrice);
|
||||
this.floatPosition.TradingAmountAvg = _.round(Number(this.floatPosition.TradingAmountAvg), 9);
|
||||
this.floatPosition.TradingAmountAvg = swapPricePrecision.roundForSubmit(
|
||||
this.floatPosition.TradingAmountAvg,
|
||||
this.floatPosition.UnderlyingInstrumentType,
|
||||
'grossPrice');
|
||||
this.floatPosition.TradingFee = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFee);
|
||||
this.floatPosition.TradingFeePending = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFeePending);
|
||||
this.floatPosition.DividendIn = parseFloat(this.floatPosition.DividendIn).toFixed(2);
|
||||
@@ -243,8 +252,10 @@ const vue = new Vue({
|
||||
main.post("/underlying_manager/GetUnderlyingPriceByCode",
|
||||
{ code: thisObj.floatPosition.UnderlyingCode, valuedate: thisObj.deal.ValueDate })
|
||||
.done(function (res) {
|
||||
res.obj = res.obj * thisObj.multiplier;
|
||||
thisObj.floatPosition.TradingAmountAvg = _.round(Number(res.obj), 9);
|
||||
thisObj.floatPosition.TradingAmountAvg = swapPricePrecision.roundForSubmit(
|
||||
swapPricePrecision.shiftDecimal(res.obj, thisObj.multiplier === 100 ? 2 : 0),
|
||||
thisObj.floatPosition.UnderlyingInstrumentType,
|
||||
'grossPrice');
|
||||
thisObj.calcFloatClosePnl();
|
||||
});
|
||||
},
|
||||
@@ -492,5 +503,6 @@ const vue = new Vue({
|
||||
components: {
|
||||
'vue-datepicker': FastVue.vueDatePicker(),
|
||||
'vue-number-input': FastVue.vueNumberInput(),
|
||||
'vue-swap-price-input': swapPricePrecision.createVueInputComponent(),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -217,4 +217,4 @@ var main = main || {};
|
||||
|
||||
global.otcformat = _format;
|
||||
|
||||
}(window));
|
||||
}(window));
|
||||
|
||||
@@ -590,4 +590,4 @@
|
||||
};
|
||||
};
|
||||
|
||||
}(window.FastVue));
|
||||
}(window.FastVue));
|
||||
|
||||
Reference in New Issue
Block a user