Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
Tags
- 자동매매
- isa
- ace 미국30년국채액티브(h)
- SCHD
- 티스토리챌린지
- 토스
- Python
- 개인연금
- 퇴직연금
- 삼성전자
- chatGPT
- JEPQ
- 미국주식
- 매일매수
- 국장
- rise 200고배당커버드콜atm
- 강환국
- 직투
- 오블완
- ETF
- S&P500
- 재테크
- OXY
- 업비트
- N잡
- 삼성전자우
- 앱테크
- IRP
- 주식
- 연금
Archives
- Today
- Total
재테크 A2Z
2025.08.25 블룸버그 스타일로 ETF 표준편차 구하기 본문
INPUT 시트의 A2: D2에 종목명, 티커, 기간, 밴드기간을 입력하면 표준편차를 구해주는 구글시트 & 앱스크립트이다.
볼린저밴드 차트까지 만들려고 했으나 실패했다. 뭐가 꼬인거 같은데 내실력으로 거기까지는 구현하지 못했다.
모로가도 서울만 가면 된다고 볼린저밴드매매 에서 입력해서 그리면 된다. 다음에 시간이 더되면 해봐야겠다. 이틀동안 붙잡고 있더니 더 헷갈린다.
https://docs.google.com/spreadsheets/d/1QPyWWhiALJDFh1BeJSybURxhKRbH-N5HS5JrWwdAuRE/edit?usp=sharing
표준편차매매법
ABCDEFGHIJKLMNOPQRSTUVWXY종목명ticker기간(일)밴드 기간(일)빠른 가이드단타·짧은 스윙(1–2주): n ≈ 10 → 반응 빠름(신호 많음, 훼이크↑)Kodex 미국S&P500KRX:37980036520스윙(약 한 달): n ≈ 20 → 균형(기본값
docs.google.com
const NUM_BINS = 40;
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('σ툴')
.addItem('템플릿(재)구성', 'setup')
.addItem('히스토그램 재생성', 'makeHistogramChart')
.addToUi();
}
function setup() {
const ss = SpreadsheetApp.getActive();
const inputS = ss.getSheetByName('Input');
if (!inputS) throw new Error('Input 시트가 필요합니다.');
const name = String(inputS.getRange('A2').getDisplayValue() || '').trim();
const ticker = String(inputS.getRange('B2').getDisplayValue() || '').trim();
const period = Number(inputS.getRange('C2').getValue() || 0);
const bandN = Number(inputS.getRange('D2').getValue() || 20); // 볼린저 N
if (!name || !ticker || !period) throw new Error('Input!A2:D2를 채워주세요.');
const sh = ensureSheet_(ss, name, true);
// ===== 1) 본표 A:J (G=BB Mid, J=Signal) =====
sh.getRange('A1:J1').setValues([[
'Date','Close','Return','MA(N)','BB Low(-2σ)','-1σ','BB Mid(MA)','+1σ','BB High(+2σ)','Signal'
]]);
// A:B (가격)
sh.getRange('A2').setFormula(
'=QUERY(GOOGLEFINANCE(Input!B2,"close",TODAY()-Input!C2,TODAY()),"select Col1, Col2 offset 1",0)'
);
// C (로그수익률)
sh.getRange('C2').setFormula('=ARRAYFORMULA({""; IFERROR(LN(B3:B / B2:B), )})');
// 포맷
sh.getRange('A:A').setNumberFormat('yyyy-mm-dd');
sh.getRange('B:B').setNumberFormat('#,##0');
sh.getRange('C:C').setNumberFormat('0.00%');
sh.setFrozenRows(1);
SpreadsheetApp.flush();
waitForReturnsReady_(sh, 15000);
// ===== 2) MA & σ (가격 기반 볼린저) D:I =====
const last = lastDataRow_(sh, 1);
for (let r = 2; r <= last; r++) {
if (r < 1 + bandN) { sh.getRange(r, 4, 1, 6).setValues([['','','','','','']]); continue; }
const a = r - bandN + 1, b = r;
sh.getRange(r, 4).setFormula(`=AVERAGE(B${a}:B${b})`); // D: MA
sh.getRange(r, 5).setFormula(`=D${r}-2*STDEV.S(B${a}:B${b})`); // E: -2σ
sh.getRange(r, 6).setFormula(`=D${r}-STDEV.S(B${a}:B${b})`); // F: -1σ
sh.getRange(r, 8).setFormula(`=D${r}+STDEV.S(B${a}:B${b})`); // H: +1σ
sh.getRange(r, 9).setFormula(`=D${r}+2*STDEV.S(B${a}:B${b})`); // I: +2σ
}
sh.getRange('D:I').setNumberFormat('#,##0');
// ===== 3) Mid(G) 배열수식 =====
sh.getRange('G2').setFormula('=ARRAYFORMULA(IF(D2:D="","",D2:D))'); // Mid = MA
// ===== 4) Signal (±1σ=F/H 기준) → J열 =====
sh.getRange('J2').setFormula(
'=ARRAYFORMULA(IF(B2:B="","",IF((H2:H="")+(F2:F="")>0,"",IF(B2:B>=H2:H,"SELL",IF(B2:B<=F2:F,"BUY","")))))'
);
// ===== 5) 요약 통계(가로) M1:T2 =====
sh.getRange('M1:T1').setValues([[
'평균(μ)','표준편차(σ)','표본수(N)','bin 폭','−2σ','−1σ','+1σ','+2σ'
]]);
sh.getRange('M2').setFormula('=IFERROR(AVERAGE(FILTER(C2:C,ISNUMBER(C2:C))),)'); // μ(로그수익률)
sh.getRange('N2').setFormula('=IFERROR(STDEV.S(FILTER(C2:C,ISNUMBER(C2:C))),)'); // σ(로그수익률)
sh.getRange('O2').setFormula('=IFERROR(COUNT(C2:C),)');
sh.getRange('P2').setFormula('=IF(OR(M2="",N2=""),"",0.2*N2)');
sh.getRange('Q2').setFormula('=IF(OR(M2="",N2=""),"",M2-2*N2)');
sh.getRange('R2').setFormula('=IF(OR(M2="",N2=""),"",M2-N2)');
sh.getRange('S2').setFormula('=IF(OR(M2="",N2=""),"",M2+N2)');
sh.getRange('T2').setFormula('=IF(OR(M2="",N2=""),"",M2+2*N2)');
sh.getRange('M2:N2').setNumberFormat('0.00%');
sh.getRange('P2:T2').setNumberFormat('0.00%');
sh.getRange('O2').setNumberFormat('0');
applySummaryColors_(sh);
// ===== 6) 히스토그램 원데이터 AA:AG =====
sh.getRange('AA1:AG1').setValues([[
'Bin Center(수익률)','히스토그램 건수','정규분포 추정건수','-2σ','-1σ','+1σ','+2σ'
]]);
sh.getRange('AA2').setFormula(
`=IF($M$2="","",SEQUENCE(${NUM_BINS},1,$M$2-4*$N$2 + $P$2/2,$P$2))`
);
const lastBinRow = 1 + NUM_BINS;
for (let r = 2; r <= lastBinRow; r++) {
sh.getRange(r, 28).setFormula(
`=IF($M$2="","",COUNTIFS(C$2:C, ">="&(AA${r}-$P$2/2), C$2:C, "<"&(AA${r}+$P$2/2)))`
);
sh.getRange(r, 29).setFormula(
`=IF($M$2="","",NORM.DIST(AA${r}, $M$2, $N$2, FALSE)*$O$2*$P$2)`
);
const maxRange = `$AB$2:$AB$${lastBinRow}`;
sh.getRange(r, 30).setFormula(`=IF($M$2="","",IF(ABS(AA${r}-$Q$2)<$P$2/2, MAX(${maxRange}), ))`);
sh.getRange(r, 31).setFormula(`=IF($M$2="","",IF(ABS(AA${r}-$R$2)<$P$2/2, MAX(${maxRange}), ))`);
sh.getRange(r, 32).setFormula(`=IF($M$2="","",IF(ABS(AA${r}-$S$2)<$P$2/2, MAX(${maxRange}), ))`);
sh.getRange(r, 33).setFormula(`=IF($M$2="","",IF(ABS(AA${r}-$T$2)<$P$2/2, MAX(${maxRange}), ))`);
}
sh.getRange(2, 27, NUM_BINS, 1).setNumberFormat('0.00%');
sh.getRange(2, 28, NUM_BINS, 6).setNumberFormat('0');
// ===== 7) (보조표 사용 안 함) =====
// ===== 8) 조건부서식(J열만) =====
applySignalFormatting_(sh);
// ===== 9) 차트 =====
SpreadsheetApp.flush();
waitForStatsReady_(sh, 8000);
removeChartAt_(sh, 13, 4); // 히스토그램 M4
makeHistogramChart();
}
// ---- 히스토그램 → M4
function makeHistogramChart() {
const ss = SpreadsheetApp.getActive();
const name = ss.getSheetByName('Input').getRange('A2').getDisplayValue().trim();
const ticker = ss.getSheetByName('Input').getRange('B2').getDisplayValue();
const sh = ss.getSheetByName(name);
if (!sh) return;
if (sh.getRange('M2').getValue() === "" || sh.getRange('O2').getValue() <= 1) return;
const range = sh.getRange(1, 27, 1 + NUM_BINS, 7); // AA1:AG
const chart = sh.newChart().asComboChart()
.addRange(range).setNumHeaders(1)
.setOption('title', `${name} (${ticker}) 수익률 분포 & 정규추정`)
.setOption('width', 900)
.setOption('height', 600)
.setOption('legend', { position: 'top' })
.setOption('hAxis', { title: '수익률(%)', format: '0.00%' })
.setOption('vAxis', { title: '건수' })
.setOption('series', {
0: { type: 'bars' },
1: { type: 'line', curveType: 'function' },
2: { type: 'line' }, 3: { type: 'line' }, 4: { type: 'line' }, 5: { type: 'line' }
})
.setPosition(4, 13, 0, 0)
.build();
removeChartAt_(sh, 13, 4);
sh.insertChart(chart);
}
// ===== 조건부서식(J열만) =====
function applySignalFormatting_(sh) {
const lastRow = sh.getMaxRows();
const colJ = sh.getRange(2, 10, lastRow - 1, 1); // J열
const buyRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied('=$J2="BUY"')
.setBackground('#D6E4FF').setFontColor('#0B5394')
.setRanges([colJ]).build();
const sellRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied('=$J2="SELL"')
.setBackground('#F8D7DA').setFontColor('#A61C1C')
.setRanges([colJ]).build();
sh.setConditionalFormatRules([buyRule, sellRule]);
sh.getRange('J:J').setHorizontalAlignment('center').setFontWeight('bold');
}
// ===== 요약 통계 가독성 =====
function applySummaryColors_(sh) {
sh.getRange('M1:T1').setBackground('#F5F5F5').setFontWeight('bold');
sh.getRange('M2:T2').setFontWeight('normal').setFontColor('#222');
sh.getRange('M2').setBackground('#E3F2FD').setFontWeight('bold').setFontColor('#0D47A1'); // μ
sh.getRange('N2').setBackground('#FFF3E0').setFontWeight('bold').setFontColor('#E65100'); // σ
sh.getRange('O2:P2').setBackground('#FAFAFA');
sh.getRange('Q2').setBackground('#FFEBEE').setFontColor('#B71C1C').setFontWeight('bold'); // −2σ
sh.getRange('T2').setBackground('#FFEBEE').setFontColor('#B71C1C').setFontWeight('bold'); // +2σ
sh.getRange('R2:S2').setBackground('#E8F5E9').setFontColor('#1B5E20').setFontWeight('bold'); // −1/+1
}
// ===== 유틸 =====
function ensureSheet_(ss, name, clear) {
let s = ss.getSheetByName(name);
if (!s) s = ss.insertSheet(name);
if (clear) {
s.clear();
s.clearFormats();
s.setConditionalFormatRules([]);
s.getCharts().forEach(c => s.removeChart(c));
}
return s;
}
function removeChartAt_(sheet, anchorCol, anchorRow) {
sheet.getCharts().forEach(c => {
const info = c.getContainerInfo();
if (info.getAnchorColumn() === anchorCol && info.getAnchorRow() === anchorRow) {
sheet.removeChart(c);
}
});
}
function lastDataRow_(sheet, col) {
const values = sheet.getRange(1, col, sheet.getMaxRows(), 1).getValues();
for (let i = values.length - 1; i >= 0; i--) {
if (values[i][0] !== '' && values[i][0] != null) return i + 1;
}
return 1;
}
function waitForReturnsReady_(sh, maxMs) {
const t0 = Date.now();
while (Date.now() - t0 < maxMs) {
SpreadsheetApp.flush();
const last = lastDataRow_(sh, 3);
const vals = sh.getRange(2, 3, Math.max(0, last - 1), 1).getValues().flat();
const cnt = vals.filter(v => typeof v === 'number' && !isNaN(v)).length;
if (cnt > 10) return true;
Utilities.sleep(300);
}
return false;
}
function waitForStatsReady_(sh, maxMs) {
const t0 = Date.now();
while (Date.now() - t0 < maxMs) {
SpreadsheetApp.flush();
const m = sh.getRange('M2').getValue();
const n = sh.getRange('N2').getValue();
const cnt = Number(sh.getRange('O2').getValue());
if (typeof m === 'number' && typeof n === 'number' && cnt > 1) return true;
Utilities.sleep(300);
}
return false;
}'재테크' 카테고리의 다른 글
| 📈 월 2회 분배금 받기 전략: 커버드콜 ETF로 절세와 현금흐름 잡기 (0) | 2025.08.28 |
|---|---|
| 2025.08.27 SCHD 기반 커버드콜 비추하는 이유 (1) | 2025.08.27 |
| 2025.08.24 구글시트 + 앱스스크립트로 만드는 볼린저밴드 알림 서비스 (3) | 2025.08.24 |
| 2025.08.24 ISA (1) | 2025.08.24 |
| 2025.08.23 미국채30년커버드콜 ETF 비교 (3) | 2025.08.23 |
