~~NOCACHE~~ ====== 연습장 ====== ====== 사진첩(러시아) ======
function openBuyForm(){
SpreadsheetApp.getUi()
.showModalDialog(HtmlService.createHtmlOutputFromFile("buyForm")
.setWidth(400).setHeight(520), "매수 입력");
}
function getNaverStockName(ticker){
try{
const url = "https://m.stock.naver.com/api/stock/" + ticker + "/basic";
const res = UrlFetchApp.fetch(url, {
muteHttpExceptions: true,
headers: {
"User-Agent": "Mozilla/5.0",
"Referer": "https://m.stock.naver.com/"
}
});
const data = JSON.parse(res.getContentText());
return data.stockName || "";
}catch(e){
return "";
}
}
function saveBuy(d){
const sh = SpreadsheetApp.getActive().getSheetByName("매수");
const r = sh.getLastRow()+1;
// 종목명 찾기
const name = findStockNameFromAll(d.ticker);
// 날짜 형식 통일
const formattedDate = formatDateYMD(d.date);
const row = [
formattedDate,
d.acc,
name,
"'" + d.ticker,
d.market,
d.price,
d.qty,
d.total,
d.target,
d.stopp,
"",
d.type,
d.reason,
""
];
sh.getRange(r,1,1,row.length).setValues([row]);
// 비중 계산 (기존 로직)
const lastDataRow = sh.getLastRow();
if(lastDataRow >= 5){
const hRange = sh.getRange(5,8,lastDataRow-4,1);
const hValues = hRange.getValues().flat().map(v => Number(v)||0);
const totalH = hValues.reduce((a,b)=>a+b,0);
if(totalH>0){
for(let i=0; i r[1].toString().includes(keyword))
.slice(0, 20);
}
function findStockName(ticker, type){
const ss = SpreadsheetApp.getActive();
const sh = (type === "ETF")
? ss.getSheetByName("ETF")
: ss.getSheetByName("주식종목");
const data = sh.getRange(2,1,sh.getLastRow()-1,2).getValues();
// 숫자/문자/앞자리 0 전부 제거 → 6자리 통일
const t = String(ticker).replace(/[^0-9A-Z]/gi,'').padStart(6,'0');
for(let i=0;i= 5){
const hRange = sh.getRange(5,8,lastDataRow-4,1); // H5~마지막 행
const hValues = hRange.getValues().flat().map(v => Number(v)||0);
const totalH = hValues.reduce((a,b)=>a+b,0);
if(totalH>0){
for(let i=0; i= 5
? sellSheet.getRange(5,1,sellLast-4,7).getValues()
: [];
// 상태 초기화 (A~K)
if(status.getLastRow() >= 2){
status.getRange(2,1,status.getLastRow()-1,11).clearContent();
}
const map = {};
// =========================
// 매수 반영
// =========================
buyData.forEach(r=>{
const acc = r[1];
const name = r[2];
const ticker = String(r[3]).replace("'","");
const qty = Number(r[6]);
const total = Number(r[7]);
const type = r[11];
if(!acc || !ticker || !qty) return;
const key = acc+"|"+ticker;
if(!map[key]){
map[key] = {acc,name,ticker,qty:0,total:0,type};
}
map[key].qty += qty;
map[key].total += total;
});
// =========================
// 매도 반영 (수량 차감)
// =========================
sellData.forEach(r=>{
const acc = r[1];
const name = r[2];
const ticker = String(r[3]).replace("'","");
const qty = Number(r[6]);
if(!acc || !ticker || !qty) return;
const key = acc+"|"+ticker;
if(!map[key]){
map[key] = {acc,name,ticker,qty:0,total:0,type:""};
}
map[key].qty -= qty; // ⭐ 핵심
});
const out = [];
Object.values(map).forEach(v=>{
if(v.qty <= 0) return; // 전량 매도면 상태에서 제거
const avg = v.qty ? Math.round(v.total / v.qty) : "";
const accShort = v.acc === "위탁종합" ? "위" : v.acc === "ISA" ? "I" : v.acc;
out.push([
accShort, // A
v.name, // B
"", // C 손실률
"", // D 매도여부
"", // E 현재가
"", // F 시간
avg, // G 평균매수가
"", // H 최고가
v.type, // I 구분
v.ticker, // J 티커
v.qty // ⭐ K 수량
]);
});
if(out.length){
status.getRange(2,1,out.length,11).setValues(out);
}
}
function getNaverRealtimePrice(ticker){
try{
const url = "https://polling.finance.naver.com/api/realtime/domestic/stock/" + ticker;
const res = UrlFetchApp.fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0",
"Referer": "https://finance.naver.com"
},
muteHttpExceptions: true
});
const json = JSON.parse(res.getContentText());
if(!json.datas || json.datas.length === 0) return null;
const price = json.datas[0].closePrice;
return Number(price.replace(/,/g,""));
}catch(e){
return null;
}
}
function updateStatusRealtime(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("상태");
if(!sh) return;
const lastRow = sh.getLastRow();
if(lastRow < 2) return;
const tickers = sh.getRange(2,10,lastRow-1,1).getValues().flat(); // J열
const avgs = sh.getRange(2,7,lastRow-1,1).getValues().flat(); // G열
for(let i=0;i{
const acc = r[1];
const name = r[2];
const ticker = r[3];
const price = Number(String(r[5]).replace(/,/g,""));
const qty = Number(r[6]);
const type = r[11];
if(!ticker || !qty) return;
const key = acc+"_"+ticker;
if(!map[key]){
map[key] = {acc,name,ticker,type,sum:0,qty:0};
}
map[key].sum += price*qty;
map[key].qty += qty;
});
st.getRange(2,1,st.getLastRow(),10).clearContent();
let row=2;
Object.values(map).forEach(o=>{
const avg = Math.round(o.sum/o.qty);
const accShort = o.acc=="위탁종합" ? "위" : (o.acc=="ISA"?"I":o.acc);
st.getRange(row,1,1,10).setValues([[
accShort,o.name,"","", "", "", avg,"",o.type,o.ticker
]]);
row++;
});
}
function getNaverPrice(t){
try{
const url = "https://polling.finance.naver.com/api/realtime/domestic/stock/"+t;
const res = UrlFetchApp.fetch(url,{muteHttpExceptions:true});
const json = JSON.parse(res.getContentText());
if(!json.datas || !json.datas.length) return null;
return Number(json.datas[0].closePrice.replace(/,/g,""));
}catch(e){
return null;
}
}
function updateStatusPrice(){
const sh = SpreadsheetApp.getActive().getSheetByName("상태");
const last = sh.getLastRow();
if(last<2) return;
for(let i=2;i<=last;i++){
const t = sh.getRange(i,10).getValue();
const avg = sh.getRange(i,7).getValue();
if(!t || !avg) continue;
const price = getNaverPrice(t);
if(!price) continue;
const loss = Math.round((price-avg)/avg*1000)/10;
sh.getRange(i,5).setValue(price);
sh.getRange(i,3).setValue(loss);
sh.getRange(i,6).setValue(new Date());
}
}
function updateStatusRealtimePrice() {
const sh = SpreadsheetApp.getActive().getSheetByName("상태");
const lastRow = sh.getLastRow();
if (lastRow < 2) return;
const tickers = sh.getRange("J2:J" + lastRow).getValues();
const priceRange = sh.getRange("E2:E" + lastRow); // 현재가
const lossRange = sh.getRange("C2:C" + lastRow); // 손실률
const avgRange = sh.getRange("G2:G" + lastRow); // 평균매수가
const highRange = sh.getRange("H2:H" + lastRow); // 최고가
const timeRange = sh.getRange("F2:F" + lastRow); // 업데이트시간
let prices = priceRange.getValues();
let losses = lossRange.getValues();
let avgs = avgRange.getValues();
let highs = highRange.getValues();
let times = timeRange.getValues();
for (let i = 0; i < tickers.length; i++) {
const t = tickers[i][0];
if (!t) continue;
try {
const url = "https://polling.finance.naver.com/api/realtime/domestic/stock/" + t;
const res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
const data = JSON.parse(res.getContentText());
if (!data || !data.datas || data.datas.length === 0) continue;
const row = data.datas[0];
const price = Number(row.closePrice.replace(/,/g, ""));
const todayHigh = Number(row.highPrice.replace(/,/g, ""));
const naverTimeStr = parseNaverTime(data.time);
if (price) prices[i][0] = price;
if (naverTimeStr) times[i][0] = naverTimeStr;
// 손실률
if (price && avgs[i][0]) {
losses[i][0] = Math.round(((price - avgs[i][0]) / avgs[i][0]) * 1000) / 10;
}
// 최고가
if (!highs[i][0] || todayHigh > highs[i][0]) {
highs[i][0] = todayHigh;
}
// 업데이트 시간
} catch (e) {
Logger.log("FAIL : " + t);
}
}
priceRange.setValues(prices);
lossRange.setValues(losses);
highRange.setValues(highs);
timeRange.setValues(times);
}
function parseNaverTime(t){
if(!t || t.length !== 14) return "";
const h = t.substr(8,2);
const mi = t.substr(10,2);
const s = t.substr(12,2);
return `${h}:${mi}:${s}`;
}
function openSellForm(){
SpreadsheetApp.getUi()
.showModalDialog(
HtmlService.createHtmlOutputFromFile("sellForm")
.setWidth(420)
.setHeight(620),
"매도 입력"
);
}
function getSellCandidates(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("상태");
const lastRow = sh.getLastRow();
if(lastRow < 2) return [];
// A:계좌 B:종목명 G:평균매수가 J:티커 K:수량
const data = sh.getRange(2,1,lastRow-1,11).getValues();
const result = [];
data.forEach(r=>{
const acc = r[0]; // 위 / I
const name = r[1];
const avg = r[6];
const ticker = r[9]; // J열
const qty = r[10]; // K열 ⭐
if(name && ticker && qty){
const accFull = acc === "위" ? "위탁종합" : acc === "I" ? "ISA" : acc;
result.push([accFull, name, ticker, avg, qty]);
}
});
return result;
}
function saveSell(d){
const sh = SpreadsheetApp.getActive().getSheetByName("매도");
const r = Math.max(sh.getLastRow()+1, 5);
const profitRate = ((d.sell - d.buy) / d.buy * 100).toFixed(1);
const profitAmt = (d.sell - d.buy) * d.qty;
const row = [
d.date,
d.acc,
d.name,
"'" + d.ticker,
d.buy,
d.sell,
d.qty,
profitRate,
profitAmt,
d.reason,
d.buyReview,
d.sellReview,
d.lesson
];
sh.getRange(r,1,1,row.length).setValues([row]);
updateStatusSheet();
}